brust 2.0.0-alpha.5
Guide / Store

Store

A store is a named bag of signals that a loader, a native component and a React island all read and write. One name, one value, three surfaces.

Define a store

components/demos/team-store.ts
// components/demos/team-store.ts
import { defineStore, signal } from '@brust/core/store'

export const team = defineStore('team', () => ({
  members: signal<TeamMember[]>([]),
  max: signal(6),
}))

Keep the name free of . and [, and keep the store in its own module, imported by the components that use it. @brust/core/store is React-free.

Seed it on the server

On the server every request gets its own instance. Write it in a loader, and the page carries the values as a snapshot, so the first paint is already right, with or without JavaScript.

pages/TeamPage.tsx
// pages/TeamPage.tsx: the page loader seeds this request's store
export async function teamPageLoader({ req }: LoaderCtx) {
  team.members.set(teamFromCookie(req.cookies.team))
  team.max.set(MAX_TEAM)
  return { ... }
}

This site is built to ship as static files, so it has no request to seed from: its pages are painted with an empty team, and the browser fills the store from its own storage when the demo mounts.

Read it anywhere

useStore from @brust/core/store/react works in both tiers. In a native component the read becomes a live binding; in an island it is useSyncExternalStore over the same browser instance.

components/demos/TeamBadge.tsx
// components/demos/TeamBadge.tsx: native
import { useStore } from '@brust/core/store/react'
import { team } from './team-store'

export default function TeamBadge() {
  const s = useStore(team)
  return <p>Team <b>{s.members.length}</b> / {s.max}</p>
}
components/demos/TeamIsland.tsx
// components/demos/TeamIsland.tsx: react (useReducer makes it an island)
const { members, max } = useStore(team)   // plain values, not signals
const [open, toggle] = useReducer(flip, true)

useStore alone compiles to the native tier. An island needs another React feature to be React, here useReducer.

Try it

Three components, two tiers, one store. Add a Pokémon with the native picker or remove one in the React island: the native badge follows both, with no reload. The team is the one from the Actions page.

store “team”TeamBadge · native

native Team 0 / 6

store “team”TeamIsland · react
  • Empty. Add Pokémon on the Actions page or below.
store “team”TeamPicker · native

Team 0 / 6. Add a seventh to see the 409.

Rules and limits

  • In a native component, read only top-level signal(...) keys, write only in handlers and effects, and read through the view (s.members), never team.members() in render. Each rule has a named diagnostic, and breaking one falls back to React.
  • Map, Set and Date serialize to {}, a BigInt throws, functions and undefined are dropped.
  • Navigating without a reload does not carry a snapshot.
Note

A cached page carries its store snapshot. Do not cache pages that paint per-user store data, such as a team seeded from the reader's cookie.