Skip to content

@paretojs/core/store

Built-in state management powered by Immer. See State Management for concepts, guidance on when to use global vs. context stores, and performance tips.

import { defineStore, defineContextStore } from '@paretojs/core/store'

Create a global reactive store. Supports direct destructuring.

const counterStore = defineStore((set) => ({
count: 0,
increment: () => set((draft) => { draft.count++ }),
}))
// Usage
const { count, increment } = counterStore.useStore()
PropertyTypeDescription
useStore()() => StateReact hook — re-renders on state change
getState()() => StateGet current state outside React
setState(fn)(fn: (draft) => void) => voidUpdate state with Immer draft
subscribe(fn)(fn: () => void) => () => voidListen for changes, returns unsubscribe

Create a per-instance store with React context. SSR-safe (no shared global state between requests). Use this when the store holds per-request data like the current user or auth tokens. See State Management — When to use global vs. context stores for guidance.

const { Provider, useStore } = defineContextStore((set) => ({
theme: 'light',
toggle: () => set((d) => { d.theme = d.theme === 'light' ? 'dark' : 'light' }),
}))
// Wrap in Provider
<Provider>
<App />
</Provider>
// Use in child components
const { theme, toggle } = useStore()
PropertyTypeDescription
ProviderReact.FC<PropsWithChildren>Context provider — wrap your component tree
useStore()() => StateReact hook — reads from the nearest Provider

For components that read a small slice of a large store, you can build a manual selector using getState() and subscribe() to avoid re-rendering when unrelated state changes:

import { useSyncExternalStore } from 'react'
const appStore = defineStore((set) => ({
count: 0,
theme: 'light',
notifications: [],
increment: () => set((d) => { d.count++ }),
addNotification: (n) => set((d) => { d.notifications.push(n) }),
}))
// Only re-renders when `count` changes
function CountBadge() {
const count = useSyncExternalStore(
appStore.subscribe,
() => appStore.getState().count
)
return <span className="badge">{count}</span>
}

For most components, useStore() with direct destructuring is sufficient and recommended. The selector pattern is an optimization for specific performance-sensitive cases — use it when profiling shows unnecessary re-renders.

The set function receives an Immer draft — you can mutate it directly:

set((draft) => {
draft.items.push(newItem) // push to array
draft.count++ // increment
delete draft.temp // delete property
draft.nested.value = 'new' // deep mutation
})

Immer ensures immutability under the hood. Each set() call produces a new state object, which triggers re-renders in components that use the store. You never need to spread or clone state manually.

During server-side rendering, global stores (defineStore) are serialized into the HTML as a <script> tag containing the store’s state. On the client, the store reads this serialized state during hydration, so the client starts with the exact same state the server rendered.

This is automatic — you do not need to write any hydration code. The sequence is:

  1. Server: Loader populates data → store is updated → React renders → store state is serialized into HTML
  2. Client: HTML loads → store reads serialized state → React hydrates with matching state → no flash of default values

Context stores (defineContextStore) also participate in SSR hydration. Their state is serialized per-Provider, so each context instance hydrates independently.

If a store is updated after hydration (e.g., by a user interaction), the new state is not serialized — it lives only on the client until the next navigation or page reload.