Implement Modern React State Management
Modern React state management guide: Redux Toolkit, Zustand, Jotai, and React Query patterns with optimistic updates.
Why it matters
Master diverse React state management strategies, from local component state to global stores and server state synchronization, ensuring efficient and scalable application architecture.
Outcomes
What it gets done
Select the optimal state management solution (Redux Toolkit, Zustand, Jotai, React Query, SWR) based on project needs.
Implement global state, server state, and URL state management patterns.
Debug and optimize state-related issues for improved performance.
Migrate legacy state management patterns to modern, efficient solutions.
Install
Add it to your toolbox
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/ag-react-state-management | bash Overview
React State Management
A React state management guide covering Redux Toolkit, Zustand, Jotai, and React Query patterns - including optimistic updates and combining client with server state. Use when setting up global state, choosing a state library, managing server state, implementing optimistic updates, or migrating from legacy Redux.
What it does
React State Management covers modern patterns across five state categories: local (component-specific, useState/useReducer), global (shared, Redux Toolkit/Zustand/Jotai), server (remote/cached, React Query/SWR/RTK Query), URL (route/search params, React Router/nuqs), and form (React Hook Form/Formik). Selection criteria are explicit: a small app with simple state fits Zustand or Jotai, a large app with complex state fits Redux Toolkit, heavy server interaction fits React Query plus light client state, and atomic/granular updates fit Jotai.
When to use - and when NOT to
Use it when setting up global state, choosing between Redux Toolkit/Zustand/Jotai, managing server state with React Query or SWR, implementing optimistic updates, or migrating from legacy Redux. React Query handles server state with a query-key factory, stale/garbage-collection times (a common default is staleTime: 5 * 60 * 1000 for 5 minutes and gcTime: 30 * 60 * 1000 for 30 minutes - gcTime was formerly named cacheTime), and optimistic mutations that snapshot, update, and roll back on error:
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// Query keys factory
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
}
// Mutation with optimistic update
export function useUpdateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
await queryClient.cancelQueries({ queryKey: userKeys.detail(newUser.id) })
const previousUser = queryClient.getQueryData(userKeys.detail(newUser.id))
queryClient.setQueryData(userKeys.detail(newUser.id), newUser)
return { previousUser }
},
onError: (err, newUser, context) => {
queryClient.setQueryData(userKeys.detail(newUser.id), context?.previousUser)
},
onSettled: (data, error, variables) => {
queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) })
},
})
}
Inputs and outputs
Five worked patterns: basic Zustand with devtools/persist middleware, Redux Toolkit with typed hooks and an createAsyncThunk slice tracking loading/error state, scalable Zustand built from composable slices with selective subscriptions to avoid unnecessary re-renders, Jotai atomic state (basic, derived, atomWithStorage-persisted, async/Suspense-enabled, and write-only action atoms), and combining Zustand for client UI state with React Query for server state in the same component.
Integrations
Best practices: colocate state near where it's used, use selectors for selective subscriptions, normalize nested data, fully type state, and keep server state (React Query) and client state (Zustand) as separate concerns rather than duplicating server data into a global store. Explicit anti-patterns: don't over-globalize state that doesn't need to be shared, don't store derived data (compute it instead), don't mutate state directly, and don't mix multiple state paradigms for the same category. A migration example shows converting a hand-written Redux reducer/action-type pair into a Redux Toolkit createSlice that uses Immer-style "mutations" internally.
Who it's for
React developers choosing or migrating a state management approach who need concrete decision criteria (app size, state category, server-vs-client) and working patterns for Redux Toolkit, Zustand, Jotai, and React Query rather than starting from a blank slate.
FAQ
Common questions
Discussion
Questions & comments ยท 0
Sign In Sign in to leave a comment.