# AI agents Seitu ships agent skills with the npm package. Install them once, then have your agent read the reference for the primitive it needs. ```bash npx skills add letstri/seitu ``` To use files from the **installed package version**, copy `node_modules/seitu/skills/seitu-overview`, `node_modules/seitu/skills/seitu` and `node_modules/seitu/skills/seitu-setup` into your agent's skills directory. The overview covers the shared API and which primitive to choose; `seitu/references/` contains the exact options and examples; `seitu-setup` moves an existing project onto Seitu. The docs are also machine-readable: | URL | Use | | ---------------------------------- | ---------------------------------------------------------------------- | | [`/llms.txt`](/llms.txt) | Find a page. | | [`/llms-full.txt`](/llms-full.txt) | Read all pages. | | `/docs/.mdx` | Read one page, such as [`/docs/core/store.mdx`](/docs/core/store.mdx). | Each page has a **Copy Markdown** button. ## Prompt for a task [#prompt-for-a-task] ```txt Use Seitu for [describe the state or browser value]. Read the installed seitu-overview skill and the relevant seitu reference first. Choose the smallest matching primitive and use the binding for this project's framework. Create shared handles once at module scope; use a factory for component-local handles. Check read-only vs writable methods, SSR defaults, and types. ``` ## Prompt for migrating a project [#prompt-for-migrating-a-project] ```txt Set up Seitu in this project. Follow the installed seitu-setup skill: detect the stack, list the code Seitu can replace, and show me the plan before you edit files. Keep existing storage keys so users keep their data. ``` Framework bindings are included in `seitu`; import them from `seitu/react`, `seitu/vue`, `seitu/solid`, or `seitu/svelte` as appropriate. # Computed ## createComputed() [#createcomputed] `transform` is memoized on source references, so `get()` is stable until a source changes. ```ts twoslash import { createComputed, createStore } from 'seitu' const count = createStore({ a: 1, b: 2 }) const sum = createComputed(count, s => s.a + s.b) sum.get() // 3 ``` ```ts twoslash import { createComputed, createStore } from 'seitu' const a = createStore(1) const b = createStore(2) const sum = createComputed([a, b], ([a, b]) => a + b) sum.get() ``` # Debounced Fn ## createDebouncedFn() [#createdebouncedfn] Each call resets the timer; the return value becomes the current state. ```ts twoslash import { createDebouncedFn } from 'seitu' const search = createDebouncedFn((query: string) => fetch(`/api?q=${query}`), 300) search.subscribe(result => console.log('result:', result)) search('hello') // debounced — fires after 300ms of inactivity search.get() search.flush() search.cancel() ``` # Debounced ## createDebounced() [#createdebounced] Unsubscribed `get()` reads through; subscribed `get()` returns the last emitted value. ```ts twoslash import { createStore, createDebounced } from 'seitu' const store = createStore('') const debounced = createDebounced(store, 300) debounced.subscribe(value => console.log('debounced:', value)) debounced.flush() ``` # Schema Store ## createSchemaStore() [#createschemastore] Invalid values fall back to `defaultValue`. Validation is memoized on the raw state reference. ```ts twoslash import { createSchemaStore } from 'seitu' import * as z from 'zod' const store = createSchemaStore({ schema: z.object({ count: z.number(), name: z.string() }), defaultValue: { count: 0, name: '' }, }) store.get() store.set({ count: 1, name: 'alice' }) store.subscribe(console.log) ``` # Store ## createStore() [#createstore] ```ts twoslash import { createStore } from 'seitu' const store = createStore({ count: 0 }) store.set(prev => ({ ...prev, count: prev.count + 1 })) store.subscribe(state => console.log(state)) store.get() // { count: 1 } ``` # Subscription ## createSubscription() [#createsubscription] * `onFirstSubscribe` attaches on first subscriber and cleans up on last. * `notify` calls every subscriber; the first throw is rethrown after the loop. * `size` is the subscriber count. ```ts twoslash import { createSubscription } from 'seitu' const { subscribe, notify, size } = createSubscription({ onFirstSubscribe: () => { const id = setInterval(notify, 1000) return () => clearInterval(id) }, }) ``` # Throttled Fn ## createThrottledFn() [#createthrottledfn] First call fires immediately; later calls within `wait` batch into one trailing call. ```ts twoslash import { createThrottledFn } from 'seitu' const log = createThrottledFn((msg: string) => console.log(msg), 300) log.subscribe(result => console.log('result:', result)) log('hello') // fires immediately log('world') // throttled — fires after 300ms log.get() log.flush() ``` # Throttled ## createThrottled() [#createthrottled] Unsubscribed `get()` reads through; subscribed `get()` returns the last emitted value. ```ts twoslash import { createStore, createThrottled } from 'seitu' const store = createStore('') const throttled = createThrottled(store, 300) throttled.subscribe(value => console.log('throttled:', value)) ``` # Custom primitives Built-in primitives use two public helpers: [`createSubscription`](/docs/core/subscription) and `createReadableSubscription`. You can use them too. A handle you build this way works with `useSubscription` in React, Vue, Solid and Svelte, with `createComputed`, and with debounce and throttle. | Helper | Gives you | | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `createSubscription({ onFirstSubscribe })` | `subscribe`, `notify` and `size`. `onFirstSubscribe` runs when the first subscriber arrives. The function it returns runs after the last subscriber leaves. | | `createReadableSubscription(get, subscribe, notify, getServer?)` | A `Readable & Subscribable` handle. Subscribers receive `get()` each time you call `notify()`. | ## Read-only: listen to the browser [#read-only-listen-to-the-browser] Attach listeners in `onFirstSubscribe`, so a handle that nothing subscribes to costs nothing. Pass `getServer` to set the value used during SSR and hydration. ```ts twoslash title="page-visibility.ts" import { createReadableSubscription, createSubscription } from 'seitu' export function createPageVisibility() { const { subscribe, notify } = createSubscription({ onFirstSubscribe: () => { document.addEventListener('visibilitychange', notify) return () => document.removeEventListener('visibilitychange', notify) }, }) const get = () => typeof document === 'undefined' || document.visibilityState === 'visible' return createReadableSubscription(get, subscribe, notify, () => true) } ``` ## Writable: add `set()` [#writable-add-set] Spread the readable handle and add `set`. Accept a value or an updater, like the built-ins do, and call `notify()` after the value changes. ```ts twoslash title="search-param.ts" import { createReadableSubscription, createSubscription } from 'seitu' import type { Readable, Subscribable, Writable } from 'seitu' export interface SearchParam extends Readable, Subscribable, Writable {} export function createSearchParam(name: string): SearchParam { const { subscribe, notify } = createSubscription({ onFirstSubscribe: () => { window.addEventListener('popstate', notify) return () => window.removeEventListener('popstate', notify) }, }) const get = () => typeof location === 'undefined' ? null : new URLSearchParams(location.search).get(name) return { ...createReadableSubscription(get, subscribe, notify, () => null), set: (value) => { const next = typeof value === 'function' ? value(get()) : value const url = new URL(location.href) if (next === null) url.searchParams.delete(name) else url.searchParams.set(name, next) history.replaceState(history.state, '', url) notify() }, } } ``` ## Rules [#rules] * **Call `notify()` after the value changes**, never before. Subscribers read `get()` when they are notified. * **Keep `get()` cheap and pure.** Bindings call it on every render. Cache the value if reading it is expensive. * **Guard browser globals.** `get()` can run on the server. Check `typeof window` or `typeof document` and pass `getServer` so the first client render matches the server HTML. * **Attach listeners lazily.** Put `addEventListener`, observers and timers in `onFirstSubscribe`, and return the cleanup. # Introduction Seitu gives stores, browser storage, and browser state one small API. Use a handle in plain TypeScript or subscribe to it in React, Vue, Solid, or Svelte. ## Install [#install] npm pnpm yarn bun ```bash npm install seitu ``` ```bash pnpm add seitu ``` ```bash yarn add seitu ``` ```bash bun add seitu ``` Import stores from `seitu`, browser primitives from `seitu/web`, and your framework binding from `seitu/react`, `seitu/vue`, `seitu/solid`, or `seitu/svelte`. Bindings ship in the same package. Storage values need a [Standard Schema](https://standardschema.dev) validator such as Zod, Valibot, or ArkType. ## Use a handle [#use-a-handle] ```tsx twoslash 'use client' import { createStore } from 'seitu' import { useSubscription } from 'seitu/react' const count = createStore(0) // shared by every importer export default function Counter() { const value = useSubscription(count) return } ``` | Method | Behavior | | ------------------------------------- | ------------------------------------------------------------------------------ | | `get()` | Read the current value. | | `subscribe(callback, { immediate? })` | Receive changes; returns an unsubscribe function. | | `set(value \| updater)` | Change a **writable** value. Browser state such as media queries is read-only. | Create shared handles at module scope. For a value owned by one component, pass a factory to its framework's `useSubscription` binding. ## Pick a primitive [#pick-a-primitive] | Need | Use | | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | In-memory value or derived value | [`createStore`](/docs/core/store), [`createComputed`](/docs/core/computed) | | Validated `localStorage` or `sessionStorage` | [`createWebStorageValue`](/docs/web/web-storage-value) for one key; [`createWebStorage`](/docs/web/web-storage) for several | | A persisted value the server must read | [`createCookieValue`](/docs/web/cookie-value) | | IndexedDB data | [`createIndexedDb`](/docs/web/indexed-db) | | Media, online, or scroll state | [`seitu/web`](/docs/web/media-query) | Browser storage uses defaults during server rendering and reads persisted values after hydration. For a server-rendered persisted value, use a cookie with `getServerCookies`. Building with an agent? [Install the Seitu skills](/docs/ai-agents). Writing your own handle? See [Custom primitives](/docs/custom-primitives). # Components ## Subscription() [#subscription] Prefer over the hook when you want a component API. ### Basic usage [#basic-usage] ```tsx twoslash title="/app/page.tsx" 'use client' import { createWebStorage } from 'seitu/web' import { Subscription } from 'seitu/react' import * as z from 'zod' const sessionStorage = createWebStorage({ type: 'sessionStorage', schemas: { count: z.number(), name: z.string() }, defaultValues: { count: 0, name: '' }, }) export default function Page() { return ( {(value) =>
{value.count}
}
) } ``` ### With selector [#with-selector] ```tsx twoslash title="/app/page.tsx" 'use client' import { createWebStorage } from 'seitu/web' import { Subscription } from 'seitu/react' import * as z from 'zod' const sessionStorage = createWebStorage({ type: 'sessionStorage', schemas: { count: z.number(), name: z.string() }, defaultValues: { count: 0, name: '' }, }) export default function Page() { return ( v.count}> {(count) =>
{count}
}
) } ``` # Hooks ## useSubscription() [#usesubscription] Pass a handle or a factory (run once; recreated when `deps` change). Sources with `getServer` (every `seitu/web` handle) use it for SSR and hydration; others use `get()`. `getSnapshot` skips selector/equality when source, selector, and version are unchanged — keep `selector` stable. Factories run during render (twice under StrictMode); keep them cheap and create long-lived resources at module scope. ### Inline subscription [#inline-subscription] ```tsx twoslash title="/app/page.tsx" 'use client' import { createWebStorageValue } from 'seitu/web' import { useSubscription } from 'seitu/react' import * as z from 'zod' export default function Page() { const value = useSubscription(() => createWebStorageValue({ type: 'sessionStorage', key: 'test', defaultValue: 0, schema: z.number(), })) return
{value}
} ``` ### Instance outside of component [#instance-outside-of-component] ```tsx twoslash title="/app/page.tsx" 'use client' import { createWebStorage } from 'seitu/web' import { useSubscription } from 'seitu/react' import * as z from 'zod' const sessionStorage = createWebStorage({ type: 'sessionStorage', schemas: { count: z.number(), name: z.string() }, defaultValues: { count: 0, name: '' }, }) export default function Page() { const value = useSubscription(sessionStorage) return
{value.count}
} ``` ### Subscription with selector [#subscription-with-selector] ```tsx twoslash title="/app/page.tsx" 'use client' import { createWebStorage } from 'seitu/web' import { useSubscription } from 'seitu/react' import * as z from 'zod' const sessionStorage = createWebStorage({ type: 'sessionStorage', schemas: { count: z.number(), name: z.string(), }, defaultValues: { count: 0, name: '' }, }) export default function Page() { // Re-renders only when count changes const count = useSubscription(sessionStorage, { selector: value => value.count }) return
{count}
} ``` ### Element from a callback ref [#element-from-a-callback-ref] ```tsx twoslash title="/app/page.tsx" 'use client' import * as React from 'react' import { createScrollState } from 'seitu/web' import { useSubscription } from 'seitu/react' export default function Page() { // A callback ref plus `deps` rebuilds on remount; `() => ref.current` binds once. const [el, setEl] = React.useState(null) const state = useSubscription( () => createScrollState({ element: el, direction: 'vertical' }), { deps: [el] } ) return (
{String(state.top.reached)}
) } ``` # Components ## Subscription() [#subscription] Prefer over the primitive when you want a component API. ### Basic usage [#basic-usage] ```tsx import { createWebStorage } from 'seitu/web' import { Subscription } from 'seitu/solid' import * as z from 'zod' const sessionStorage = createWebStorage({ type: 'sessionStorage', schemas: { count: z.number(), name: z.string() }, defaultValues: { count: 0, name: '' }, }) function Page() { return ( {value =>
{value().count}
}
) } ``` ### With selector [#with-selector] ```tsx import { createWebStorage } from 'seitu/web' import { Subscription } from 'seitu/solid' import * as z from 'zod' const sessionStorage = createWebStorage({ type: 'sessionStorage', schemas: { count: z.number(), name: z.string() }, defaultValues: { count: 0, name: '' }, }) function Page() { return ( v.count}> {count =>
{count()}
}
) } ``` # Hooks ## useSubscription() [#usesubscription] Pass a handle or a getter (reactive: re-subscribes when it reads a signal). Returns an `Accessor` (`value()`). ### Inline subscription [#inline-subscription] ```tsx import { createWebStorageValue } from 'seitu/web' import { useSubscription } from 'seitu/solid' import * as z from 'zod' function Counter() { const value = useSubscription(() => createWebStorageValue({ type: 'sessionStorage', key: 'test', defaultValue: 0, schema: z.number(), })) return
{value()}
} ``` ### Instance outside of component [#instance-outside-of-component] ```tsx import { createWebStorage } from 'seitu/web' import { useSubscription } from 'seitu/solid' import * as z from 'zod' const sessionStorage = createWebStorage({ type: 'sessionStorage', schemas: { count: z.number(), name: z.string() }, defaultValues: { count: 0, name: '' }, }) function Counter() { const value = useSubscription(sessionStorage) return
{value().count}
} ``` ### Subscription with selector [#subscription-with-selector] ```tsx import { createWebStorage } from 'seitu/web' import { useSubscription } from 'seitu/solid' import * as z from 'zod' const sessionStorage = createWebStorage({ type: 'sessionStorage', schemas: { count: z.number(), name: z.string(), }, defaultValues: { count: 0, name: '' }, }) function Counter() { // Accessor updates only when count changes const count = useSubscription(sessionStorage, { selector: value => value.count }) return
{count()}
} ``` ### Reactive source (re-subscribes when a signal changes) [#reactive-source-re-subscribes-when-a-signal-changes] ```tsx import { createSignal } from 'solid-js' import { createWebStorageValue } from 'seitu/web' import { useSubscription } from 'seitu/solid' import * as z from 'zod' function User() { const [userId, setUserId] = createSignal('user-1') const data = useSubscription(() => createWebStorageValue({ type: 'localStorage', key: `user:${userId()}`, schema: z.object({ name: z.string() }), defaultValue: { name: '' }, })) return
{data().name}
} ``` ### Ref example [#ref-example] ```tsx import { createScrollState } from 'seitu/web' import { useSubscription } from 'seitu/solid' function Page() { let ref: HTMLDivElement | undefined const state = useSubscription(() => createScrollState({ element: () => ref, direction: 'vertical' })) return (
{String(state().top.reached)}
) } ``` # Hooks ## useSubscription() [#usesubscription] Pass a handle or a factory (run once). Returns a Svelte `Readable` (`$value`); it attaches on first subscriber and detaches on last. ### Inline subscription [#inline-subscription] ```svelte
{$value}
``` ### Instance outside of component [#instance-outside-of-component] ```svelte
{$value.count}
``` ### With selector [#with-selector] ```svelte
{$count}
``` # Composables ## useSubscription() [#usesubscription] Pass a handle or a ref/getter that returns one. ### Inline subscription [#inline-subscription] ```vue ``` ### Instance outside of the subscription [#instance-outside-of-the-subscription] ```vue ``` ### With selector [#with-selector] ```vue ``` # Cookie Value ## createCookieValue() [#createcookievalue] The cookie is the storage, so the server and the browser read the same value: pass `getServerCookies` and SSR renders the visitor's choice, and hydration matches it. `set` writes `document.cookie` and is a no-op on the server; sending a `Set-Cookie` response header is the framework's job. Keep values small: the encoded `name=value` pair must stay under 4 KB (bigger writes are skipped with a warning) and cookies travel with every request. `HttpOnly` cookies are invisible to JavaScript, so they cannot be read or written here. ### Vanilla [#vanilla] ```ts twoslash title="theme.ts" import { createCookieValue } from 'seitu/web' import * as z from 'zod' const theme = createCookieValue({ key: 'theme', schema: z.enum(['light', 'dark']), defaultValue: 'light', }) theme.get() theme.set('dark') theme.subscribe(console.log) theme.clear() ``` ### TanStack Start [#tanstack-start] ```ts title="theme.ts" import { createIsomorphicFn } from '@tanstack/react-start' import { getRequestHeader } from '@tanstack/react-start/server' import { createCookieValue } from 'seitu/web' import * as z from 'zod' export const theme = createCookieValue({ key: 'theme', schema: z.enum(['light', 'dark']), defaultValue: 'light', getServerCookies: createIsomorphicFn().server(() => getRequestHeader('cookie') ), }) ``` ### Next.js [#nextjs] ```ts title="theme.ts" import { cookies } from 'next/headers' import { createCookieValue } from 'seitu/web' import * as z from 'zod' export const theme = createCookieValue({ key: 'theme', schema: z.enum(['light', 'dark']), defaultValue: 'light', getServerCookies: () => cookies() .getAll() .map(({ name, value }) => `${name}=${value}`) .join('; '), }) ``` ### React [#react] ```tsx twoslash title="page.tsx" 'use client' import { createCookieValue } from 'seitu/web' import { useSubscription } from 'seitu/react' import * as z from 'zod' const theme = createCookieValue({ key: 'theme', schema: z.enum(['light', 'dark']), defaultValue: 'light', }) export default function Page() { const value = useSubscription(theme) return ( ) } ``` # IndexedDB ## createIndexedDb() [#createindexeddb] Missing stores/indexes are created automatically; concurrent opens of the same name are serialized. `version` is a minimum, not a pin: adding a store or an index bumps it on its own, so set it only when existing rows need migrating. Bump it, then rewrite the rows with `migrate` in `onUpgrade`, which runs inside the `versionchange` transaction after the declared stores and indexes are created. Everything there is synchronous — `migrate` queues a cursor walk instead of returning a promise, and `onUpgrade` cannot be `async`. `migrate` rewrites fields, not keys: changing the `keyPath` field of a row aborts the upgrade. Indexes are created and filled before `onUpgrade` runs, so a migration cannot clean up rows for a `unique` index added in the same version — add the index in a later version than the cleanup. A failed upgrade rolls back whole and is reported with `console.warn`: `ready` still resolves, but reads and writes then reject, because every access retries the same failing upgrade. Every write is one transaction, and only one. A single `put`, `delete`, `set` or `clear` call opens a `readwrite` transaction and queues all of its rows or keys on it, so a batch lands whole or not at all; two calls are two transactions, and none of them spans stores. Reads work the same way: each one is its own `readonly` snapshot. ### Vanilla [#vanilla] ```ts twoslash title="db.ts" import { createIndexedDb, createIndexedDbStorage, createIndexedDbTable } from 'seitu/web' import * as z from 'zod' const db = createIndexedDb({ name: 'app', stores: { settings: createIndexedDbStorage({ schemas: { theme: z.enum(['light', 'dark']) }, defaultValues: { theme: 'light' }, }), todos: createIndexedDbTable({ keyPath: 'id', indexes: { status: 'status' }, schema: z.object({ id: z.string(), title: z.string(), status: z.enum(['open', 'done']) }), }), }, }) const { settings, todos } = db.stores settings.get() // { theme: 'light' } until hydrated await db.ready await todos.put({ id: '1', title: 'Write docs', status: 'open' }) ``` ### Migrations [#migrations] ```ts twoslash title="db.ts" import { createIndexedDb, createIndexedDbTable } from 'seitu/web' import * as z from 'zod' const db = createIndexedDb({ name: 'app', version: 2, // v1 rows have no `priority` stores: { todos: createIndexedDbTable({ keyPath: 'id', indexes: { priority: 'priority' }, schema: z.object({ id: z.string(), title: z.string(), priority: z.number() }), }), }, onUpgrade: ({ oldVersion, migrate }) => { if (oldVersion < 2) { migrate('todos', row => ({ ...row, priority: row.priority ?? 0 })) // Return `null` to drop a row, nothing to keep it as is. } }, }) await db.ready ``` ### Transactions [#transactions] ```ts twoslash title="checkout.ts" import { createIndexedDb, createIndexedDbStorage, createIndexedDbTable } from 'seitu/web' import * as z from 'zod' const db = createIndexedDb({ name: 'app', stores: { sync: createIndexedDbStorage({ schemas: { lastSyncedAt: z.number(), pending: z.number() }, defaultValues: { lastSyncedAt: 0, pending: 0 }, }), todos: createIndexedDbTable({ keyPath: 'id', schema: z.object({ id: z.string(), title: z.string(), status: z.enum(['open', 'done']) }), }), }, }) const { sync, todos } = db.stores // One `readwrite` transaction: both rows land, or neither does. await todos.put([ { id: '1', title: 'Write docs', status: 'done' }, { id: '2', title: 'Ship docs', status: 'open' }, ]) await todos.delete(['1', '2']) await sync.set({ lastSyncedAt: Date.now(), pending: 0 }) await todos.put({ id: '3', title: 'Reconcile', status: 'open' }) await sync.set({ pending: 1 }) ``` ## createIndexedDbStorage() [#createindexeddbstorage] Key/value store for `createIndexedDb({ stores })`. `get()` reads an in-memory cache hydrated from IndexedDB (`await db.ready`). `set`/`clear` update the cache now and persist later. SSR snapshot is `defaultValues`. ### Vanilla [#vanilla-1] ```ts twoslash title="settings-storage.ts" import { createIndexedDb, createIndexedDbStorage } from 'seitu/web' import * as z from 'zod' const db = createIndexedDb({ name: 'app', stores: { settings: createIndexedDbStorage({ schemas: { token: z.string().nullable(), preferences: z.object({ theme: z.enum(['light', 'dark']) }), }, defaultValues: { token: null, preferences: { theme: 'light' } }, }), }, }) const { settings } = db.stores settings.get() await db.ready await settings.set({ token: 'abc' }) settings.get() // { token: 'abc', preferences: { theme: 'light' } } settings.subscribe(console.log) ``` ### React [#react] ```tsx twoslash title="page.tsx" 'use client' import { createIndexedDb, createIndexedDbStorage } from 'seitu/web' import { useSubscription } from 'seitu/react' import * as z from 'zod' const db = createIndexedDb({ name: 'app', stores: { settings: createIndexedDbStorage({ schemas: { count: z.number(), name: z.string() }, defaultValues: { count: 0, name: '' }, }), }, }) export default function Page() { const value = useSubscription(db.stores.settings) return (
{value.count} {value.name}
) } ``` ## createIndexedDbTable() [#createindexeddbtable] Row store for `createIndexedDb({ stores })`: keyed rows, indexes, range reads, validation. Use `query()` for a `Readable`/`Subscribable` that re-runs on table changes. On the server, queries stay on `initial`. Key paths must name a schema field that can hold a key, and reads are typed from that field. Compound (`['id', 'order']`) and nested (`'meta.slug'`) paths fall back to `IDBValidKey`. ### Vanilla [#vanilla-2] ```ts twoslash title="todos.ts" import { createIndexedDb, createIndexedDbTable } from 'seitu/web' import * as z from 'zod' const db = createIndexedDb({ name: 'app', stores: { todos: createIndexedDbTable({ keyPath: 'id', indexes: { status: 'status' }, schema: z.object({ id: z.string(), title: z.string(), status: z.enum(['open', 'done']) }), }), }, }) const { todos } = db.stores await todos.put({ id: '1', title: 'Write docs', status: 'open' }) await todos.get('1') await todos.getAll() await todos.index('status').getAll('open') // index names and keys are typed await todos.delete('1') const open = todos.query(t => t.index('status').getAll('open'), { initial: [] }) open.get() open.subscribe(rows => console.log(rows)) ``` ### React [#react-1] ```tsx twoslash title="page.tsx" 'use client' import { createIndexedDb, createIndexedDbTable } from 'seitu/web' import { useSubscription } from 'seitu/react' import * as z from 'zod' const db = createIndexedDb({ name: 'app', stores: { todos: createIndexedDbTable({ keyPath: 'id', schema: z.object({ id: z.string(), title: z.string(), status: z.enum(['open', 'done']) }), }), }, }) const allTodos = db.stores.todos.query(t => t.getAll(), { initial: [] }) export default function Page() { const rows = useSubscription(allTodos) return (
    {rows.map(todo =>
  • {todo.title}
  • )}
) } ``` # Is Online ## createIsOnline() [#createisonline] On the server, `get()` and the SSR snapshot are `true`. ### Vanilla [#vanilla] ```ts twoslash import { createIsOnline } from 'seitu/web' const isOnline = createIsOnline() isOnline.subscribe(value => { console.log(value ? 'online' : 'offline') }) console.log(isOnline.get()) ``` ### React [#react] ```tsx twoslash title="page.tsx" import { createIsOnline } from 'seitu/web' import { useSubscription } from 'seitu/react' const isOnline = createIsOnline() function Status() { const online = useSubscription(isOnline) return online ? 'Connected' : 'Disconnected' } ``` # Media Query ## createMediaQuery() [#createmediaquery] On the server, `get()` and the SSR snapshot are `defaultMatches` (`false`). ### Vanilla [#vanilla] ```ts twoslash title="media-query.ts" import { createMediaQuery } from 'seitu/web' import { useSubscription } from 'seitu/react' const isDesktop = createMediaQuery({ query: '(min-width: 768px)' }) isDesktop.subscribe(matches => { console.log(matches) }) const state = isDesktop.get() console.log(state) ``` ### React [#react] ```tsx twoslash title="page.tsx" import { createMediaQuery } from 'seitu/web' import { useSubscription } from 'seitu/react' const isDesktop = createMediaQuery({ query: '(min-width: 768px)' }) function Layout() { const matches = useSubscription(isDesktop) return matches ? 'i am desktop' : 'i am mobile' } ``` ### Errors [#errors] ```tsx twoslash import { createMediaQuery } from 'seitu/web' import { useSubscription } from 'seitu/react' // @errors: 2362 2322 1109 createMediaQuery({ query: '(min-width: ' }) // @errors: 2362 2322 2820 createMediaQuery({ query: '(min-width: 768' }) ``` # Scroll State ## createScrollState() [#createscrollstate] Notifies on `scroll` and `ResizeObserver` (not inner content growth — call `'~'.notify()` after that). The element getter is resolved on first subscribe; recreate the handle if the element identity changes (React: `deps`). ### Vanilla [#vanilla] ```ts twoslash import { createScrollState } from 'seitu/web' const scroll = createScrollState({ element: document.querySelector('.container'), direction: 'vertical', threshold: 10, }) scroll.subscribe(state => { console.log(state.top.reached) console.log(state.top.remaining) console.log(state.bottom.reached) console.log(state.bottom.remaining) }) const state = scroll.get() console.log(state) ``` ### React (callback ref) [#react-callback-ref] ```tsx twoslash title="page.tsx" 'use client' import * as React from 'react' import { createScrollState } from 'seitu/web' import { useSubscription } from 'seitu/react' function Layout() { const [ref, setRef] = React.useState(null) const state = useSubscription(() => createScrollState({ element: ref, threshold: 10, }), { deps: [ref] }) return (
{state.top.reached ? 'at the top' : 'scrolled'}
) } ``` # Web Storage Value ## createWebStorageValue() [#createwebstoragevalue] On the server, `get()` and the SSR snapshot are `defaultValue`. ### Vanilla [#vanilla] ```ts twoslash title="session-storage.ts" import { createWebStorageValue } from 'seitu/web' import * as z from 'zod' const tokenStorage = createWebStorageValue({ type: 'sessionStorage', key: 'token', schema: z.string().nullable(), defaultValue: null, }) tokenStorage.get() tokenStorage.set('abc') tokenStorage.get() tokenStorage.subscribe(console.log) ``` ### React [#react] ```tsx twoslash title="page.tsx" 'use client' import { createWebStorageValue } from 'seitu/web' import { useSubscription } from 'seitu/react' import * as z from 'zod' const countStorage = createWebStorageValue({ type: 'sessionStorage', key: 'count', schema: z.number(), defaultValue: 0, }) export default function Page() { const value = useSubscription(countStorage) return (
{value}
) } ``` # Web Storage ## createWebStorage() [#createwebstorage] On the server, `get()` and the SSR snapshot are `defaultValues`. ### Vanilla [#vanilla] ```ts twoslash title="session-storage.ts" import { createWebStorage } from 'seitu/web' import * as z from 'zod' const sessionStorage = createWebStorage({ type: 'sessionStorage', schemas: { token: z.string().nullable(), preferences: z.object({ theme: z.enum(['light', 'dark']) }), }, defaultValues: { token: null, preferences: { theme: 'light' } }, }) sessionStorage.get() sessionStorage.set({ token: 'abc' }) sessionStorage.get() // { token: 'abc', preferences: { theme: 'light' } } sessionStorage.subscribe(console.log) ``` ### React [#react] ```tsx twoslash title="page.tsx" 'use client' import { createWebStorage } from 'seitu/web' import { useSubscription } from 'seitu/react' import * as z from 'zod' const sessionStorage = createWebStorage({ type: 'sessionStorage', schemas: { count: z.number(), name: z.string() }, defaultValues: { count: 0, name: '' }, }) export default function Page() { const value = useSubscription(sessionStorage) return (
{value.count} {value.name}
) } ```