Singleton Fallback
Why the singleton fallback exists
Section titled “Why the singleton fallback exists”Not every store needs a StoreProvider. Auth state, theme preferences, feature flags — these are genuinely global. There is one instance per app, and every component reads from it.
For these stores, the Provider pattern adds unnecessary ceremony. You would create the instance, wrap the entire app, and pass it through context — only to end up with the exact same behavior as a singleton.
The singleton fallback eliminates that ceremony. When useStore(Def) is called outside of any StoreProvider, it automatically creates and returns a global singleton instance:
import { useStore } from '@hurum/react'import { AuthStore } from './store'
function UserMenu() { const store = useStore(AuthStore) const user = store.use.currentUser() const loggedIn = store.use.isLoggedIn() // computed
if (!loggedIn) return <LoginButton /> return <span>Hello, {user.name}</span>}No Provider, no setup, no boilerplate.
How it works
Section titled “How it works”store.use.*
Section titled “store.use.*”Every field in your store’s state (including computed values) becomes a hook on the use proxy:
const store = useStore(CounterStore)const count = store.use.count() // raw stateconst doubled = store.use.doubled() // computedconst displayName = store.use.displayName() // computed from userEach hook creates a useSyncExternalStore subscription for that specific field. When count changes but multiplier stays the same, only components reading count re-render.
store.useSelector
Section titled “store.useSelector”For derived state that combines multiple fields:
const store = useStore(CounterStore)const summary = store.useSelector((state) => ({ count: state.count, product: state.product,}))The selector runs on every state change, but the component re-renders only when the returned value is structurally different from the previous one.
store.send
Section titled “store.send”Dispatch intents on the resolved instance:
const store = useStore(CounterStore)
store.send.plusClicked({ amount: 1 })store.send(CounterIntents.plusClicked({ amount: 1 }))store.send(CounterIntents.plusClicked, { amount: 1 })All three are equivalent. store.send is a proxy that supports both named shortcuts and direct intent dispatch.
When the singleton is created
Section titled “When the singleton is created”The global singleton is created lazily on first access — the first time useStore(Def) is called outside a StoreProvider. You do not need to initialize it explicitly.
When to stop using the singleton
Section titled “When to stop using the singleton”The singleton fallback breaks down when:
- You need multiple instances. A dashboard with three chart widgets, two purchase forms side by side, a list where each item has its own store — the singleton gives you one instance, and you need many.
- You need server data in initial state.
Store.create({ initialState })lets you inject server data. The singleton starts with default state only. - You need scoped dependencies. Different instances may need different
deps(e.g., one with a real API client, another with a mock). - You are rendering on the server. No global state on the server. Period.
When any of these apply, switch to StoreProvider + useStore().
Context-aware behavior
Section titled “Context-aware behavior”The same useStore(Def) call works in both contexts. Inside a StoreProvider, it returns the scoped instance. Outside, it returns the singleton. This means your component code does not change — only the wrapping does.
import { useStore, StoreProvider } from '@hurum/react'import { CounterStore } from './store'
function Counter() { // Same code works in both contexts const store = useStore(CounterStore) const count = store.use.count() return <p>{count}</p>}
// Singleton fallback — no Provider needed<Counter />
// Scoped instance — Provider wraps the component<StoreProvider of={CounterStore} store={scopedInstance}> <Counter /></StoreProvider>Full example
Section titled “Full example”import { useStore } from '@hurum/react'import { TodoStore } from './store'
function TodoList() { const store = useStore(TodoStore) const todos = store.use.todos() const filter = store.use.filter() const filteredTodos = store.use.filteredTodos() // computed
return ( <div> <select value={filter} onChange={(e) => store.send.filterChanged({ filter: e.target.value })} > <option value="all">All</option> <option value="active">Active</option> <option value="completed">Completed</option> </select> <ul> {filteredTodos.map((todo) => ( <li key={todo.id}> <input type="checkbox" checked={todo.completed} onChange={() => store.send.todoToggled({ id: todo.id })} /> {todo.title} </li> ))} </ul> </div> )}Next steps
Section titled “Next steps”- useStore — Context-aware access for Provider scenarios
- Provider & withProvider — Why you need scoped instances and how to use them