React Integration
@quantajs/react provides React bindings on top of @quantajs/core, built on
useSyncExternalStore so they are concurrent-safe.
The package is 7.3 kB raw / 2.8 kB gzipped as of 2.1 — Preact and the
DevTools UI are no longer bundled in, and @quantajs/devtools is an optional
peer loaded behind a dynamic import.
Installation
npm install @quantajs/react @quantajs/core
# or
pnpm add @quantajs/react @quantajs/core
# or
yarn add @quantajs/react @quantajs/core
The shortest version
// stores/cart.ts
import { defineStore } from '@quantajs/core';
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] as Item[] }),
getters: { total: (s) => s.items.reduce((n, i) => n + i.price, 0) },
actions: {
add(item: Item) { this.items.push(item); },
},
});
import { useQuanta } from '@quantajs/react';
import { useCartStore } from './stores/cart';
function Cart() {
const cart = useQuanta(useCartStore);
return (
<>
<p>{cart.items.length} items — ${cart.total}</p>
<button onClick={() => cart.add({ id: 1, price: 9.99 })}>Add</button>
</>
);
}
No provider needed. Hooks resolve against the nearest provider's container if there is one, and against the ambient container otherwise.
Choosing a hook
| Hook | Subscribes to | Use when |
|---|---|---|
useQuanta(def) | the whole store | the component reads several things and re-rendering on any change is fine |
useQuantaValue(def, sel) | only what sel reads | the component reads one slice of a store that changes often |
useQuantaActions(def) | nothing | the component only calls actions |
useLocalStore(def) | the whole store, per mount | the store belongs to one component instance |
useQuanta(definition)
Resolves the definition and subscribes to every change.
const cart = useQuanta(useCartStore);
cart.total; // getter
cart.add(item); // action
cart.checkout.pending; // action lifecycle
useQuantaValue(definition, selector, options?)
Subscribes to what the selector reads, not to the store as a whole. A
component reading s.items.length is not re-rendered by a write to
s.couponCode.
const count = useQuantaValue(useCartStore, (s) => s.items.length);
const pending = useQuantaValue(useCartStore, (s) => s.checkout.pending);
For selectors that return a fresh object each call, pass a comparator:
import { shallow } from '@quantajs/react';
const stats = useQuantaValue(
useCartStore,
(s) => ({ count: s.items.length, total: s.total }),
{ equalityFn: shallow },
);
options also accepts any equalityFn: (a: T, b: T) => boolean of your own.
Selector semantics changed in 2.1:
Selectors used to be compared by result. Against mutable proxies that was
broken in both directions: s => s.todos returned the same proxy after an
in-place mutation so nothing re-rendered, and s => s.todos.filter(…) returned
a new array every time so everything re-rendered. Tracking reads instead fixes
both. If you wrote memoisation workarounds around a selector, you can probably
delete them.
useQuantaActions(definition)
Resolves the store without subscribing. A component that only dispatches never re-renders on state changes.
function AddButton() {
const cart = useQuantaActions(useCartStore);
return <button onClick={() => cart.add(item)}>Add</button>;
}
Note:
Resolve through a hook, not by calling the definition directly inside a
component. useCartStore() bypasses the provider and reads the ambient
container, so under a <QuantaProvider container={…}> it would hand you a
different cart than the rest of the page. useQuantaActions is the
zero-subscription way to get the store from the right container.
useLocalStore(definition)
Gives each mount its own container, disposed on unmount. Two instances of the component never share state.
import { defineStore } from '@quantajs/core';
import { useLocalStore } from '@quantajs/react';
const useWizardStore = defineStore('wizard', {
state: () => ({ step: 1 }),
actions: {
next() { this.step = Math.min(3, this.step + 1); },
back() { this.step = Math.max(1, this.step - 1); },
},
});
function Wizard() {
const wizard = useLocalStore(useWizardStore);
return (
<p>
step {wizard.step}/3
<button onClick={() => wizard.back()}>back</button>
<button onClick={() => wizard.next()}>next</button>
</p>
);
}
// <Wizard /> <Wizard /> — independent.
This replaces useCreateStore, which took loose state / getters / actions
arguments and could not be typed properly. StrictMode's double
mount/unmount/remount rebuilds the container rather than handing back a
disposed store.
QuantaProvider
import { QuantaProvider } from '@quantajs/react';
<QuantaProvider> {/* creates and owns a container */}
<QuantaProvider container={container}> {/* you own its lifetime */}
<QuantaProvider snapshot={snapshot}> {/* hydrates server state */}
| Prop | Description |
|---|---|
container | The container stores resolve into. Omit and the provider creates one, disposing it on unmount. Pass one — per request — under SSR. |
snapshot | Server state from container.dehydrate(), applied during the first render rather than in an effect. |
No more `stores` prop:
2.0.0's <QuantaProvider stores={{ cart, user }}> is gone. The provider carries
a container; stores resolve into it lazily on first use, so you no longer
enumerate them up front.
Hydration happens synchronously during render on purpose. An effect runs after the first paint, so the first client render would use default state, disagree with the server's markup, and correct itself a frame later — exactly the mismatch this avoids. See SSR.
Lower-level hooks
These take a resolved store, not a definition. Reach for them when you already have the instance.
useQuantaStore(store)
const cart = useQuantaStore(resolvedCartStore);
useQuantaSelector(store, selector, options?)
const count = useQuantaSelector(resolvedCartStore, (s) => s.items.length);
useQuanta and useQuantaValue are these two plus container resolution, which
is why they are the ones to prefer.
useWatch(store, source, callback)
Runs a side effect when the watched value changes.
useWatch(cart, (s) => s.items.length, (count) => {
toast(`cart has ${count} item(s)`);
});
useComputed(store, fn)
A cached derivation that only recomputes when the values it read change. Useful for expensive derivations that do not belong in the store itself.
const cart = useQuantaActions(useCartStore);
const withTax = useComputed(cart, (s) => s.total * 1.0825);
Resolve the store with useQuantaActions here rather than useQuanta —
useComputed owns the fine-grained subscription, so a whole-store subscription
on top of it is redundant.
Async actions in components
function CheckoutButton() {
const cart = useQuantaActions(useCartStore);
const pending = useQuantaValue(useCartStore, (s) => s.checkout.pending);
const error = useQuantaValue(useCartStore, (s) => s.checkout.error);
return (
<>
<button
// The action rethrows so a caller can handle one specific call.
// This component reads `error` instead, so it silences the rejection.
onClick={() => cart.checkout().catch(() => {})}
disabled={pending}
>
{pending ? 'Checking out…' : 'Checkout'}
</button>
{pending && <button onClick={() => cart.checkout.abort()}>Cancel</button>}
{error && <span role="alert">{error.message}</span>}
</>
);
}
See Async Actions.
DevTools
DevTools is opt-in as of 2.1 and attaches to window only after you enable it:
import { enableDevTools } from '@quantajs/react';
import { QuantaDevTools } from '@quantajs/react/devtools';
if (process.env.NODE_ENV === 'development') {
enableDevTools();
}
function AppShell() {
return (
<>
<App />
<QuantaDevTools />
</>
);
}
@quantajs/devtools is an optional peer dependency loaded through a dynamic
import, so it does not enter your production bundle.
Re-exports
@quantajs/react re-exports the core API you are likely to need alongside the
hooks — defineStore, createStore, createContainer, reactive, computed,
watch, effect, batchEffects, enableDevTools and more — so a React app
can import from one place.
import { defineStore, useQuanta } from '@quantajs/react';
Tips
- Prefer
useQuantaValueoveruseQuantain views that render a lot. - Use
useQuantaActionsfor components that only dispatch. - Resolve stores through hooks, never by calling the definition inside a component — the hooks are what respect the provider's container.
- Define stores at module scope; a definition holds no state, so it is safe there.