Skip to content
brix.v2 stable

Search documentation

70 guides

Documentation menu
Docs / Pages and UIRead Markdown ↗

Stores

createStore provides request-safe SSR state and in-memory browser state shared across independently hydrated Preact islands. Use it when multiple islands on one page need the same reactive model. Keep state local with useSignal when only one component owns it; use storage when values must survive navigation, reloads, or browser tabs.

Generate an application store

brix add store booking --feature booking

The declaration belongs to the application that hydrates it:

src/features/booking/
  booking.store.ts
  BookingDetails.tsx
  BookingSummary.tsx

A store module is *.store.ts, not .tsx, and has one runtime export: the destructured named tuple returned by createStore. The provider and hook must share a stem:

import { useSignal } from "brix/hooks";
import { createStore } from "brix/islands";

type Booking = { guestName: string; status: string };

export const [BookingStoreProvider, useBookingStore] = createStore(
  ({ initialBooking }: { initialBooking: Booking }) => {
    const booking = useSignal(initialBooking);
    return {
      booking,
      confirm: () => {
        booking.value = { ...booking.value, status: "confirmed" };
      },
    };
  },
);

The provider name ends in StoreProvider; the hook starts with use and ends in Store. Type-only exports are allowed. Brix's application build transform injects a stable identity into conventional store modules. Do not provide a manual key in application code.

A library can share setup functions, domain operations, and types, but should not export the createStore tuple intended to span another application's islands. Generate the thin tuple in the owning application so its build assigns the identity.

Render one provider

Place one provider for the store above all consumers:

<BookingStoreProvider initialBooking={booking}>
  <BookingDetails />
  <BookingSummary />
  <BookingBadge />
</BookingStoreProvider>

The provider itself hydrates as an island. Children cross its server-rendered slot and do not all become part of the provider's browser bundle. See islands for slot and hydration boundaries.

Provider props use island serialization. Pass supported serializable data, including primitives, plain objects, arrays, and supported rich values; do not pass functions, symbols, arbitrary class instances, request contexts, or services. The setup return value stays in memory and is not serialized, so it may contain signals, functions, and class instances.

Only one provider for a store should be mounted on a page. A second active provider with the same identity logs an error because ownership and initialization would be ambiguous.

Consume state safely

Store setup runs in a Preact render context, so supported hooks can initialize the model. Import application component hooks from brix/hooks.

"use island";

import { useBookingStore } from "./booking.store";

export function BookingDetails() {
  const store = useBookingStore();
  if (!store) return null;

  return (
    <button type="button" onClick={store.confirm}>
      {store.booking.value.guestName}: {store.booking.value.status}
    </button>
  );
}

useBookingStore() returns the setup state or null. It is null before the provider has rendered, so every consumer must handle that state. A non-island server component may call the same hook when a provider is above it during SSR.

Cross-island state and request isolation

Each island hydrates into a separate Preact tree, so ordinary context does not cross island roots. Stores use a shared signal registry in the browser, persisted by stable store identity, allowing an update in BookingDetails to render BookingSummary even though they hydrate separately.

SSR uses a renderer-local registry instead of a process-global value. Concurrent requests and later requests cannot observe one another's provider state. The server state initializes browser state through the provider's serialized props; mutable setup return values themselves do not cross that boundary.

This makes stores suitable for page-scoped UI coordination, not server persistence, global caches, or user sessions. For server data freshness and cache ownership, see caching.

Test a store

Use a browser component test to exercise actual reactive sharing. A manual second argument to createStore is reserved for direct tests and should be unique per test:

import { expect, it } from "brix/test";
import { page, render } from "brix/test/browser";
import { h } from "brix/preact";
import { createStore } from "brix/islands";

it("shares state between roots", async () => {
  const setup = ({ value }: { value: string }) => ({ value });
  const [Provider] = createStore(setup, "booking-test");
  const [, useStore] = createStore(setup, "booking-test");

  function Consumer() {
    return h("span", { "data-testid": "value" }, useStore()?.value ?? "missing");
  }

  render(h(Provider, { value: "shared" }));
  render(h(Consumer, null));

  await expect.element(page.getByTestId("value")).toHaveTextContent("shared");
});

Also test the null fallback and provider initialization props. Route tests can verify SSR markup; browser tests prove sharing after hydration.

brix check
brix test --project browser
brix build

See testing for component and route harness choices.