Containers

A container is a set of store instances that are isolated from every other container. One per browser app, one per server request, one per test.

import { createContainer } from '@quantajs/core';

const container = createContainer();
const cart = useCartStore(container);

Why containers exist

Before 2.1, stores lived in one module-global registry. In a browser that is fine — one user, one process. On a server it is not: one Node process serves every request, so a module-scope store was a process-wide singleton. Request A writing user.email was visible to request B. No error, no warning, just another user's data.

A container makes the isolation boundary explicit and gives you three things the global registry could not:

  • Request isolation under SSR.
  • Test isolation — a fresh container per test instead of a shared registry you have to remember to reset.
  • Component-scoped stores — two <Wizard> instances that never share a step. (useLocalStore in React is a container per mount.)

The ambient container

You do not have to create one. Calling a definition with no argument resolves against the ambient container, which is created lazily on first use:

const cart = useCartStore();  // ambient

That is the right default in a browser and the wrong one on a server.

Server code:

The ambient container is shared across every request in a Node process. On a server, always create a container per request and pass it explicitly.

API

createContainer(id?)

function createContainer(id?: string): StoreContainer;

id is optional and only used for logs and DevTools; one is generated if you omit it.

StoreContainer

MemberTypeDescription
idstringStable identifier.
activebooleanfalse after dispose().
resolve(name, options)StoreCreate or return the store for this name. Idempotent.
get(name)AnyStore | undefinedA store already created in this container.
has(name)booleanWhether a store with this name exists here.
keys()string[]Names of every store in this container.
dehydrate()ContainerSnapshotSerialisable snapshot of every store's state.
hydrate(snapshot)voidApply a snapshot. Order-independent.
dispose()voidDestroy every store in this container. Idempotent.

You rarely call resolve yourself — calling a definition does it for you. It is there for the name-based escape hatch and for tooling.

Default-container helpers

import {
  getDefaultContainer,
  setDefaultContainer,
  resetDefaultContainer,
  destroyAllStores,
} from '@quantajs/core';
FunctionDescription
getDefaultContainer()The ambient container, creating it if needed.
setDefaultContainer(c)Replace the ambient container.
resetDefaultContainer()Dispose the ambient container and forget it.
destroyAllStores()Dispose every store in the ambient container. Handy in test teardown.

Patterns

One per server request

import { createContainer } from '@quantajs/core';

export async function handler(req, res) {
  const container = createContainer(req.id);
  try {
    const user = useUserStore(container);
    await user.load(req.userId);

    const html = renderToString(<App container={container} />);
    const snapshot = container.dehydrate();

    res.send(page(html, snapshot));
  } finally {
    container.dispose();
  }
}

See the SSR guide for the client half.

One per test

import { createContainer } from '@quantajs/core';
import { beforeEach, afterEach, expect, test } from 'vitest';

let container;

beforeEach(() => {
  container = createContainer();
});

afterEach(() => {
  container.dispose();
});

test('adding an item updates the total', () => {
  const cart = useCartStore(container);
  cart.add({ id: 1, price: 10 });
  expect(cart.total).toBe(10);
});

Because each test gets its own container, state cannot leak between tests and you never have to reset a shared registry. dispose() also tears down watchers, effects and persistence subscriptions, so nothing survives to fire during the next test.

One per component instance

In React, useLocalStore creates and disposes a container per mount:

import { useLocalStore } from '@quantajs/react';

function Wizard() {
  const wizard = useLocalStore(useWizardStore);
  return <p>step {wizard.step}/3</p>;
}

// Two <Wizard />s never share a step.

Sharing one container with a React tree

import { QuantaProvider } from '@quantajs/react';

<QuantaProvider container={container}>
  <App />
</QuantaProvider>

useQuanta, useQuantaValue and useQuantaActions resolve against the nearest provider's container, falling back to the ambient one when there is no provider. Omit container and QuantaProvider creates one for itself and disposes it on unmount; pass one in and its lifetime stays yours.

Lifetime

Calling dispose() destroys every store in the container and marks it inactive. Resolving against a disposed container throws rather than silently handing back dead state:

container.dispose();
useCartStore(container); // Error: Container "…": cannot resolve store "cart" after dispose().

Learn More