Skip to content

State Management

Pareto includes a built-in state management solution powered by Immer. No extra dependencies needed. Stores are reactive and support direct destructuring with per-property subscriptions.

Create a global reactive store with defineStore. The initializer function receives a set function for Immer-powered mutations and a get function for reading current state:

import { defineStore } from '@paretojs/core/store'
const counterStore = defineStore((set) => ({
count: 0,
history: [] as string[],
increment: () =>
set((draft) => {
draft.count++
draft.history.push(`+1 → ${draft.count}`)
}),
decrement: () =>
set((draft) => {
draft.count--
draft.history.push(`-1 → ${draft.count}`)
}),
reset: () =>
set((draft) => {
draft.count = 0
draft.history = []
}),
}))

Supports direct destructuring — pull out exactly the state and actions you need:

function Counter() {
const { count, increment, decrement, reset } = counterStore.useStore()
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
)
}

Each store created with defineStore returns an object with these methods. See the full Store API reference for type signatures and detailed usage.

MethodDescription
useStore()React hook — returns state + actions, re-renders on change
getState()Get current state outside React
setState(fn)Update state with Immer draft
subscribe(fn)Listen for state changes

useStore() returns a proxy object where each property getter independently calls useSyncExternalStore. When you destructure const { count } = useStore(), only count is subscribed — changes to other properties like history do not cause a re-render. No manual selectors needed.

For derived values, destructure what you need and compute from it:

function useOrderTotal() {
const { items } = orderStore.useStore()
return items.reduce((sum, item) => sum + item.price, 0)
}

The hook subscribes to items via the proxy getter. When items changes, the component re-renders and the total is recomputed. No need for subscribe or useSyncExternalStore — just useStore() and plain JavaScript.

For per-request state that is SSR-safe, use defineContextStore. Global stores created with defineStore share state across all requests on the server, which can cause data leaks between users. Context stores use React context to scope state to a single render tree:

import { defineContextStore } from '@paretojs/core/store'
const { Provider, useStore } = defineContextStore((initialUser) => (set) => ({
user: initialUser,
setUser: (user) => set((draft) => { draft.user = user }),
}))

Wrap a section of your component tree with <Provider>, passing initialData:

function App({ user }) {
return (
<Provider initialData={user}>
<Dashboard />
</Provider>
)
}
function Dashboard() {
const { user, setUser } = useStore()
// ...
}

When should I use global vs. context stores?

Section titled “When should I use global vs. context stores?”
  • Global store (defineStore) — Best for client-only state that does not vary per request: UI theme, sidebar open/closed, client-side caches. On the server, the initial state is the same for every request, so there is no cross-request contamination risk.
  • Context store (defineContextStore) — Best for per-request state: current user, auth tokens, request-specific feature flags. Because context stores are scoped to a Provider, each SSR request gets its own isolated instance.

If you are unsure, start with defineContextStore. It is always SSR-safe. You can switch to a global store later if the state truly is shared and request-independent.

For the full API reference, type signatures, and hydration details, see the Store API page.