State Management Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
Classify state first: Label every piece as server, client UI, URL/navigation, form, or secret - then match to a tool. Random library picks create overlapping caches.
Query-first for API data: TanStack Query owns lists, details, loading, errors, dedupe, and background refresh. Do not hand-roll useEffect fetch on the fifth networked screen.
Keep client UI out of Query cache: Theme toggles, wizard steps, and modal flags are not query keys. Misusing cache semantics serializes UI into server infrastructure.
Default greenfield stack: TanStack Query (server) + Zustand or split Context (client) + Expo Router search params (shareable filters). Add Redux only when an ADR requires it.
useState for ephemeral screen UI: Modal open, single-screen selection, and simple toggles stay local until another route genuinely needs the value.
useReducer for coupled transitions: Wizards, small multi-field forms, and step machines get one reducer - not five independent useState calls that drift out of sync.
Split Context by update frequency: Theme, session state, and actions live in separate contexts. One mega-context re-renders the list on every cart tick.
Memoize provider values: useMemo for state objects and useCallback for actions. Inline { signOut: () => ... } in provider value breaks React.memo downstream.
Reach for Zustand when selectors matter: Global cart, preferences, and flags need fine-grained subscriptions. Select primitives - useStore((s) => s.count) - not the whole store.
One store per domain: cartStore, preferencesStore - not appStore with forty unrelated fields. Import cycles and test resets get painful fast.
Redux Toolkit when audit matters: Regulated workflows, mandated DevTools, and middleware ecosystems justify RTK. Not for a badge counter on day one.
Do not run two server caches: Pick TanStack Query or RTK Query for the same API - never both. Migration is slice-by-slice with a checklist.
Wire mobile lifecycle for Query: onlineManager + NetInfo and focusManager + AppState in bootstrap. Web defaults do not refetch on app foreground.
Tune staleTime and gcTime deliberately: Catalog reads tolerate minutes of staleness; balances and checkout totals do not. Document defaults in createQueryClient.
Optimistic mutations with rollback: Use onMutate, cancelQueries, snapshot previous, and restore on onError. Manual optimistic useState scales poorly across screens.
Persist reads, not secrets: persistQueryClient and Zustand persist for public catalog and prefs. Access tokens go to expo-secure-store - never AsyncStorage.
Hydrate before routing: Session and persisted prefs must finish loading before choosing auth vs app stacks. Pair with splash hold to avoid flash of login screen.
Clear all layers on logout: Secure token, Zustand slices, Query cache, and disk persister. Shared tablets leak PII when only one layer clears.
URL state via Expo Router params: Filters and tabs that marketing wants shareable belong in useLocalSearchParams - not a global store that breaks deep links.
Forms stay colocated: React Hook Form or screen-level reducer for inputs; useMutation on submit. Do not stream every keystroke into Zustand.
Jotai for atomic UIs only when justified: Builder and inspector panels benefit from derived atoms. Default hires know Zustand - document Jotai in an ADR if adopted.
Test store boundaries: Reset Zustand with setState(initial, true); isolated QueryClient with retry: false. Mock network at service edge, not inside reducers.
Measure before optimizing: React DevTools Profiler on list scroll beats premature Jotai adoption. Fix provider storms before sprinkling React.memo on two hundred rows.
Version persisted schemas: Bump name: "preferences-v3" or Query buster on breaking API or shape changes. OTA without cache bust parses stale JSON into crashes.
Record the decision in an ADR: Before squad B installs MobX while squad A uses Redux, write ADR: State Management Selection and link it from store modules.
TanStack Query + useState/useReducer on screens. Add Zustand when a second route needs the same client state. Add Context for session if Zustand feels heavy for one user object.
Audit trails, time-travel debugging requirements, and existing org standards. See Redux Toolkit.
Same API data edited in Zustand and Query; token in Query cache; filter in global store but not in URL when shareable. Run the checklist in State Management Basics.
Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026