Skip to main content

Where to Keep State

The place for state is chosen by the kind of data, not by a library. State starts local and is lifted only as high as needed; what can be computed is not stored. Server state (a cache of data from an API) goes to a data-fetching library such as TanStack Query; URL state (filters, page, tab) goes to the URL; global client state goes to context if it changes rarely, or to a store (Zustand, Redux Toolkit) if it changes often and is read in parts. Most of what apps used to keep in a global store is server state.

Applies to: React 19.3, Zustand 5.

Details​

Kinds of State​

KindExampleWhere
Local UI stateAn open dropdown, the text of an inputuseState in the component
Shared by nearby componentsThe selected tab and its panelLifted to the closest common parent
DerivedA filtered list, a totalComputed during render, not stored
FormField values, validation errorsUncontrolled fields and Actions, or React Hook Form
URLFilters, sorting, page, the selected IDSearch and route params, see React Router
ServerA user profile, a product listTanStack Query, RTK Query, SWR, or framework loaders
Global client, rarely changesTheme, locale, the current userContext
Global client, changes oftenAn unsaved canvas, a selection in a table, a multi-step wizardA store: Zustand, Redux Toolkit, Jotai
Not shown in the UIA timer ID, the previous valueRef
Persisted on the deviceA draft, user settingslocalStorage behind a store (Zustand persist) or useSyncExternalStore

Client and Server State​

  • Client state belongs to the app: it is synchronous and always up to date.
  • Server state belongs to the server: the app holds a copy that can be stale, is shared with other users, and arrives asynchronously.
  • Server state needs caching, deduplication of requests, background refetching and invalidation after a change. Kept in useState or Redux, all of this is written by hand.
  • One source of truth: the same data is not copied into a second place (server data into local state, props into state). The copy goes stale.

When a Store Is Needed​

  • Many distant components read and change the same client data.
  • The data changes often and components read only parts of it: a store with selectors re-renders only the components whose part changed; context re-renders every consumer.
  • The update logic is complex and needs DevTools or middleware.

Otherwise local state, composition and context are enough.

Libraries​

LibraryModel
Redux ToolkitOne store, actions and reducers; DevTools, middleware, RTK Query
ZustandA store as a hook with selectors; no provider, little boilerplate
JotaiAtoms: small pieces of state combined bottom-up, like useState
MobXObservable objects, mutable updates, automatic tracking of reads
XStateState machines: explicit states and transitions for complex flows

Example​

A Zustand store: each component subscribes to its own part through a selector.

import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';

const useCartStore = create((set) => ({
items: [],
add: (item) => set((state) => ({ items: [...state.items, item] })),
clear: () => set({ items: [] }),
}));

function CartBadge() {
// Re-renders only when the number of items changes
const count = useCartStore((state) => state.items.length);
return <span>{count}</span>;
}

function AddButton({ product }) {
const add = useCartStore((state) => state.add);
return <button onClick={() => add(product)}>Add to cart</button>;
}

function CartSummary() {
// A new object on every call: an infinite loop in Zustand 5
// Error: Maximum update depth exceeded.
const { items, clear } = useCartStore((state) => ({
items: state.items,
clear: state.clear,
}));
const { items, clear } = useCartStore(
useShallow((state) => ({ items: state.items, clear: state.clear }))
);
// …
}

Follow-up Questions​

  • Context or a store? Context for values that change rarely; a store for frequently changing data read in parts, see Context and a Store.
  • Why not keep API data in Redux? Caching, refetching and invalidation would be written by hand. With Redux, RTK Query does it.
  • What is state colocation? Keeping state as close as possible to where it is used: fewer re-renders and easier deletion.
  • Why put filters in the URL? The link can be shared, the back button works, and the state survives a reload.
  • How does Zustand work without a provider? The store lives in a module variable outside React, and components subscribe to it through useSyncExternalStore.

Common Mistakes​

  • A global store for everything, including the text of an input.
  • Copying server data into useState or a store and syncing it with effects.
  • Storing derived data.
  • Context for data that changes on every keystroke.
  • A Zustand selector that returns a new object or array without useShallow.

See Also​