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

HookSubscribes toUse when
useQuanta(def)the whole storethe component reads several things and re-rendering on any change is fine
useQuantaValue(def, sel)only what sel readsthe component reads one slice of a store that changes often
useQuantaActions(def)nothingthe component only calls actions
useLocalStore(def)the whole store, per mountthe 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 */}
PropDescription
containerThe container stores resolve into. Omit and the provider creates one, disposing it on unmount. Pass one — per request — under SSR.
snapshotServer 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 useQuantauseComputed 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 useQuantaValue over useQuanta in views that render a lot.
  • Use useQuantaActions for 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.

Learn More