@octanejs/zustand
v0.1.55
Published
zustand bindings for the octane renderer — reuses zustand's framework-agnostic vanilla store and swaps the React binding for octane's useSyncExternalStore.
Keywords
Readme
@octanejs/zustand
zustand for the octane UI framework.
Installation
npm install @octanejs/zustand
pnpm add @octanejs/zustandzustand separates a framework-agnostic vanilla store (createStore) from a tiny
React binding (create + useStore) built on useSyncExternalStore. This package
reuses the vanilla store unchanged (re-exported verbatim from zustand/vanilla) and
reimplements only the binding on top of octane's useSyncExternalStore. The public
surface matches zustand 1:1 — most zustand code works by changing the import.
// before
import { create } from 'zustand';
// after
import { create } from '@octanejs/zustand';
const useBearStore = create((set) => ({
bears: 0,
increase: () => set((s) => ({ bears: s.bears + 1 })),
}));
function BearCounter() @{
const bears = useBearStore((s) => s.bears);
<h1>{bears as string} bears</h1>
}Entry points
| import | what you get | notes |
| --- | --- | --- |
| @octanejs/zustand | create, useStore, createStore | the React binding, octane-bound |
| @octanejs/zustand/vanilla | createStore + types | re-exported verbatim from zustand |
| @octanejs/zustand/shallow | shallow, useShallow | shallow verbatim; useShallow octane-bound |
| @octanejs/zustand/middleware | persist, devtools, subscribeWithSelector, combine, redux, createJSONStorage, … | re-exported verbatim (all framework-agnostic) |
| @octanejs/zustand/traditional | createWithEqualityFn, useStoreWithEqualityFn | octane-bound (selector + equality fn) |
How it works
octane keys hooks by a compiler-injected per-call-site Symbol, appended as the last
argument of every use* call. A custom hook is just a wrapper that forwards that
slot to the base hook it composes — which is all useStore does. Because the slot is
per-call-site, useBearStore(a) and useBearStore(b) in one component (or the same
hook used twice) stay independent, exactly like in React.
Naming matters. The hook you call must follow the
use*convention (const useBearStore = create(...)) so the compiler recognises it and injects the slot — this is the sameuse*-is-reserved-for-hooks rule React uses.
Selecting object slices — useShallow
A selector that returns a fresh object/array each call ((s) => ({ a: s.a })) never
compares Object.is-equal, so it violates useSyncExternalStore snapshot stability
and reaches the maximum update depth guard. Wrap it with useShallow to cache
the selection by shallow equality:
import { useShallow } from '@octanejs/zustand/shallow';
function Sliced() @{
const { a, b } = useBearStore(useShallow((s) => ({ a: s.a, b: s.b })));
// re-renders only when a or b actually changes
}Equality functions — traditional
For the equality-fn pattern, @octanejs/zustand/traditional provides
createWithEqualityFn / useStoreWithEqualityFn:
import { createWithEqualityFn } from '@octanejs/zustand/traditional';
import { shallow } from '@octanejs/zustand/shallow';
const useStore = createWithEqualityFn((set) => ({ a: 0, b: 0 }), shallow);
const { a } = useStore((s) => ({ a: s.a })); // bails out via shallowFor most object-slice selections, prefer useShallow — the v5-recommended approach.
Status
Current scope, known divergences, and verification status are tracked in the
generated bindings status table, sourced from
this package's status.json.
