Skip to content

Repository files navigation

Seitu

npm version You need Seitu

Type-safe reactive primitives with one contract: get(), subscribe(), set(). Works with in-memory state, validated localStorage / IndexedDB, media queries, scroll position. Bindings for React, Vue, Solid, Svelte. SSR-safe.

Documentation · Playground

pnpm add seitu

Import from an entry point (seitu, seitu/web, seitu/react, seitu/vue, seitu/solid, seitu/svelte, seitu/utils) or from a single feature to pull in nothing else:

import { createStore } from 'seitu/core/store'
import { createWebStorage } from 'seitu/web/web-storage'

Examples

State and derived values

import { createComputed, createStore } from 'seitu'

const cart = createStore({ items: [{ price: 10, qty: 2 }], coupon: 0.1 })
const total = createComputed(
  cart,
  (c) => c.items.reduce((s, i) => s + i.price * i.qty, 0) * (1 - c.coupon)
)

total.get() // 18
cart.set((c) => ({ ...c, coupon: 0 }))
total.subscribe((t) => console.log(t)) // 20

Debounce and throttle

import { createDebounced, createDebouncedFn, createStore } from 'seitu'

const query = createStore('')
createDebounced(query, 300).subscribe((q) => fetch(`/search?q=${q}`))

const save = createDebouncedFn((draft: string) => api.save(draft), 500)
save('hello') // runs after 500 ms of silence
save.flush() // or now

Validated localStorage

import { createWebStorage, createWebStorageValue } from 'seitu/web'
import * as z from 'zod'

const settings = createWebStorage({
  type: 'localStorage',
  schemas: {
    theme: z.enum(['light', 'dark']),
    sidebar: z.object({ open: z.boolean() }),
  },
  defaultValues: { theme: 'light', sidebar: { open: true } },
})

settings.set({ theme: 'dark' }) // partial update
settings.get() // { theme: 'dark', sidebar: { open: true } }, invalid data falls back to defaults

const theme = createWebStorageValue({
  type: 'localStorage',
  key: 'theme',
  schema: z.enum(['light', 'dark']),
  defaultValue: 'light',
}) // single key
theme.set('light')

Cookie the server can render

import { createCookieValue } from 'seitu/web'
import * as z from 'zod'

const lang = createCookieValue({
  key: 'lang',
  schema: z.enum(['en', 'fr']),
  defaultValue: 'en',
  getServerCookies: () => requestCookieHeader, // the framework's `Cookie` header reader
}) // SSR renders the cookie value, hydration matches it
lang.set('fr')

IndexedDB

import {
  createIndexedDb,
  createIndexedDbStorage,
  createIndexedDbTable,
} from 'seitu/web'
import * as z from 'zod'

const db = createIndexedDb({
  name: 'app',
  stores: {
    session: createIndexedDbStorage({
      schemas: { token: z.string().nullable() },
      defaultValues: { token: null },
    }), // key/value, sync get() via cache
    todos: createIndexedDbTable({
      keyPath: 'id',
      indexes: { status: 'status' },
      schema: z.object({ id: z.string(), status: z.enum(['open', 'done']) }),
    }), // rows, validated on write and read
  },
}) // one connection, stores created on demand

const { session, todos } = db.stores

await db.ready // open + session hydrated
await session.set({ token: 'abc' })
await todos.put({ id: '1', status: 'open' })
const open = todos.query((t) => t.index('status').getAll('open'), {
  initial: [],
}) // reactive

Browser state

import { createIsOnline, createMediaQuery, createScrollState } from 'seitu/web'

const isDark = createMediaQuery({ query: '(prefers-color-scheme: dark)' }) // type-checked query
const online = createIsOnline()
const feed = createScrollState({
  element: () => document.querySelector('#feed'),
  threshold: 200,
})

feed.subscribe(({ bottom }) => bottom.reached && loadMore())

Frameworks

// React
import { useSubscription } from 'seitu/react'

function Theme() {
  const theme = useSubscription(settings, { selector: (s) => s.theme })
  return (
    <button
      onClick={() =>
        settings.set({ theme: theme === 'light' ? 'dark' : 'light' })
      }
    >
      {theme}
    </button>
  )
}
<!-- Vue -->
<script setup lang="ts">
import { useSubscription } from 'seitu/vue'

const theme = useSubscription(settings, { selector: (s) => s.theme })
</script>

<template>
  <button
    @click="settings.set({ theme: theme === 'light' ? 'dark' : 'light' })"
  >
    {{ theme }}
  </button>
</template>
// Solid
import { useSubscription } from 'seitu/solid'

function Theme() {
  const theme = useSubscription(settings, { selector: (s) => s.theme })
  return (
    <button
      onClick={() =>
        settings.set({ theme: theme() === 'light' ? 'dark' : 'light' })
      }
    >
      {theme()}
    </button>
  )
}
<!-- Svelte -->
<script lang="ts">
  import { useSubscription } from 'seitu/svelte'

  const theme = useSubscription(settings, { selector: (s) => s.theme })
</script>

<button onclick={() => settings.set({ theme: $theme === 'light' ? 'dark' : 'light' })}>{$theme}</button>

Inline creation (elements, props). Use a callback ref so the subscription is rebuilt when the element mounts, unmounts, or remounts:

const [el, setEl] = useState<HTMLDivElement | null>(null)
const scroll = useSubscription(() => createScrollState({ element: el }), {
  deps: [el],
})

return <div ref={setEl} />

Agent skills (TanStack Intent)

Skills ship inside the npm package and describe the installed version's API.

pnpm dlx @tanstack/intent@latest install
pnpm dlx @tanstack/intent@latest load seitu#seitu-overview

License

MIT

About

Resources

Stars

55 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages