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
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: 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: 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: 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.
native Team 0 / 6
- Empty. Add Pokémon on the Actions page or below.
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), neverteam.members()in render. Each rule has a named diagnostic, and breaking one falls back to React. Map,SetandDateserialize to{}, aBigIntthrows, functions andundefinedare dropped.- Navigating without a reload does not carry a snapshot.
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.