Local-first React state helpers that keep the API small and the updates clear.
bun add @oneirosoft/quiddityor
npm install @oneirosoft/quidditycreate builds a local store hook. Each component that calls the hook gets its
own isolated state and actions. Updates are always partial merges of state and
are driven through a set function you define inside your actions.
import { create } from "@oneirosoft/quiddity"
const useCounter = create((set) => ({
count: 0,
label: "Clicks",
inc: (by = 1) => set((state) => ({ count: state.count + by })),
setLabel: (label: string) => set({ label }),
}))
export function Counter() {
const store = useCounter()
return (
<button onClick={() => store.inc()}>
{store.label}: {store.count}
</button>
)
}TypeScript tip:
type CounterStore = {
count: number
label: string
inc: (by?: number) => void
setLabel: (label: string) => void
}
const useCounter = create<CounterStore>((set) => ({
count: 0,
label: "Clicks",
inc: (by = 1) => set((state) => ({ count: state.count + by })),
setLabel: (label) => set({ label }),
}))combine helper:
import { combine, create } from "@oneirosoft/quiddity"
const useCounter = create(
combine({ count: 0 }, (set) => ({
inc: () => set((state) => ({ count: state.count + 1 })),
setCount: (count: number) => set({ count }),
}))
)Use combine when you want a clean separation between plain initial state and
action creators, while still getting good inference in the builder. It keeps
your initial state visible and avoids repeating fields inside actions.
import { create } from "@oneirosoft/quiddity"
const useToggle = create((set) => ({
on: false,
toggle: () => set((state) => ({ on: !state.on })),
}))
export function Toggle() {
const store = useToggle()
return <button onClick={store.toggle}>{store.on ? "On" : "Off"}</button>
}create(builder)
create(builder, derive?)
// builder signature
type Builder<S> = (set: (update: Partial<S> | ((s: S) => Partial<S>)) => void) => S
// optional derive signature
type Derive<S, D> = (state: S) => Dcreate returns a hook:
const useStore = create(...)
const store = useStore()store is the merged object of:
- your actions
- current state values
setaccepts a partial object or an updater function.- Updates are merged into current state (shallow merge).
- Only non-function keys are considered state. Functions are treated as actions.
- State, action, and derived keys must be unique. Overlaps are rejected by TypeScript and also throw at runtime.
const useCounter = create(
(set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}),
(state) => ({ doubleCount: state.count * 2 })
)
const store = useCounter()
// store.doubleCount is a derived valueDerived values are computed from state on render and are read-only. They update
whenever the underlying state changes, but they do not participate in set
updates directly.
You can also return functions from derive:
const useMath = create(
(set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}),
(state) => ({
doubleCount: state.count * 2,
multBy: (n: number) => state.count * n,
})
)
const store = useMath()
store.multBy(3) // uses the latest state- The store is local to each component instance.
- Any state update triggers a re-render of the component using the hook.
- Destructuring fewer fields does not avoid re-renders.
- To isolate re-renders, split state into multiple hooks or components.
- Keep state serializable (it is read via object entries).
- Actions can call
setmultiple times; updates are merged in order. - This library is intentionally minimal and does not provide global stores.