seitu
Version:
231 lines (179 loc) • 5.71 kB
Markdown
# Seitu
[](https://npmjs.com/package/seitu) 
Type-safe reactive primitives with one contract: `get()`, `subscribe()`, `set()`. Works with in-memory state, validated `localStorage` / IndexedDB, media queries, scroll position, element size. Bindings for React, Vue, Solid, Svelte. SSR-safe.
[Documentation](https://seitu.letstri.dev) · [Playground](https://github.com/letstri/seitu/tree/main/playground)
```bash
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:
```ts
import { createStore } from 'seitu/core/store'
import { createWebStorage } from 'seitu/web/web-storage'
```
## Examples
### State and derived values
```ts
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
```ts
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`
```ts
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
```ts
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
```ts
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
```ts
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
```tsx
// 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
<!-- 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>
```
```tsx
// 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
<!-- 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:
```tsx
const [el, setEl] = useState<HTMLDivElement | null>(null)
const scroll = useSubscription(() => createScrollState({ element: el }), {
deps: [el],
})
return <div ref={setEl} />
```
## Agent skills
Skills ship inside the npm package and describe the installed version's API.
```bash
npx skills add letstri/seitu
```
## License
MIT