Skip to content

Singleton Fallback

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.

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 state
const doubled = store.use.doubled() // computed
const displayName = store.use.displayName() // computed from user

Each hook creates a useSyncExternalStore subscription for that specific field. When count changes but multiplier stays the same, only components reading count re-render.

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.

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.

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.

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().

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>
app.tsx
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>
)
}