State Management Architecture (Redux, Zustand, Context API)
1. Redux Toolkit (RTK Query, createSlice, configureStore)
| Concept | API | Description | Use Case |
|---|---|---|---|
| Redux Toolkit NEW | @reduxjs/toolkit |
Official opinionated Redux toolset with simplified API, built-in Immer, thunks | Complex state logic, large applications, time-travel debugging |
| createSlice | createSlice() |
Generate action creators and reducers automatically with less boilerplate | Feature-based state management with auto-generated actions |
| RTK Query | createApi() |
Data fetching and caching layer built on Redux, auto-generates hooks | REST/GraphQL APIs with automatic cache invalidation |
| Immer Integration | state.value++ |
Write mutable code that produces immutable updates automatically | Simplified reducer logic, no spread operators needed |
| DevTools Extension | Redux DevTools |
Time-travel debugging, action replay, state inspection | Debugging complex state transitions, bug reproduction |
Example: Redux Toolkit with RTK Query
// store/slices/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value++; }, // Immer allows mutation
decrement: (state) => { state.value--; },
incrementByAmount: (state, action) => { state.value += action.payload; }
}
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
// store/api/postsApi.js - RTK Query
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const postsApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post'],
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post']
}),
addPost: builder.mutation({
query: (post) => ({ url: '/posts', method: 'POST', body: post }),
invalidatesTags: ['Post'] // Auto-refetch on mutation
})
})
});
export const { useGetPostsQuery, useAddPostMutation } = postsApi;
// Usage in component
function PostList() {
const { data, isLoading, error } = useGetPostsQuery();
const [addPost] = useAddPostMutation();
if (isLoading) return <div>Loading...</div>;
return <div>{data.map(post => <Post key={post.id} {...post} />)}</div>;
}
Key Benefits: 95% less boilerplate vs classic Redux, automatic
cache invalidation, optimistic updates, request deduplication, polling support
2. Zustand (Lightweight State Management, React Hooks)
| Feature | Syntax | Description | Benefit |
|---|---|---|---|
| Minimal API | create() |
Simple hook-based API without providers, actions, or reducers | 3KB bundle, zero boilerplate, easy learning curve |
| No Provider Wrapper | useStore() |
Direct store access without Context Provider hierarchy | Cleaner component tree, no context re-render issues |
| Transient Updates | subscribe() |
Subscribe to store changes without causing re-renders | Performance optimization for animations, frequent updates |
| Selector Functions | useStore(state => state.x) |
Fine-grained subscriptions to specific state slices | Prevents unnecessary re-renders, optimized performance |
| Middleware Support | persist, devtools |
Optional plugins for persistence, debugging, async actions | Flexible enhancement without core complexity |
Example: Zustand Store Implementation
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
// Simple store
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}));
// With middleware (persistence + devtools)
const useBearStore = create(
devtools(
persist(
(set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 })
}),
{ name: 'bear-storage' } // localStorage key
)
)
);
// Usage - selector prevents re-render unless count changes
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return <button onClick={increment}>{count}</button>;
}
// Transient updates (no re-render)
useStore.subscribe(
(state) => state.count,
(count) => console.log('Count changed:', count)
);
Zustand vs Redux
- Bundle Size: Zustand 3KB vs Redux Toolkit 12KB
- Boilerplate: Single create() call vs slice/store/provider setup
- Performance: Selective subscriptions prevent unnecessary renders
- Use Case: Small-to-medium apps, simple state, rapid prototyping
3. React Context API (useContext, Provider Pattern)
| Concept | API | Description | Best Practice |
|---|---|---|---|
| createContext | React.createContext() |
Create context object for passing data through component tree | Use for infrequently changing data (theme, auth, locale) |
| Context Provider | <Context.Provider> |
Wrap components to provide context value to descendants | Split contexts by update frequency to minimize re-renders |
| useContext Hook | useContext(Context) |
Subscribe to context value in functional components | Combine with useMemo to prevent unnecessary calculations |
| Re-render Issue | Every consumer re-renders |
All context consumers re-render when provider value changes | Use composition, memo, or external library for optimization |
| Prop Drilling Solution | Context + Custom Hook |
Avoid passing props through multiple component layers | Create custom useAuth(), useTheme() hooks for clean API |
Example: Optimized Context Pattern
// contexts/AuthContext.js
import { createContext, useContext, useState, useMemo } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
// Memoize value to prevent re-renders when provider re-renders
const value = useMemo(() => ({
user,
token,
login: (userData, authToken) => {
setUser(userData);
setToken(authToken);
},
logout: () => {
setUser(null);
setToken(null);
}
}), [user, token]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// Custom hook with error handling
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// Usage
function App() {
return (
<AuthProvider>
<Dashboard />
</AuthProvider>
);
}
function Dashboard() {
const { user, logout } = useAuth();
return <button onClick={logout}>{user.name}</button>;
}
Warning: Context API is not optimized for frequent updates. For
high-frequency state changes (form inputs, animations), use local state or specialized libraries like
Zustand/Jotai.
4. MobX (Observable State Trees, Reactive Programming)
| Concept | Decorator/API | Description | Advantage |
|---|---|---|---|
| Observable State | makeObservable() |
Automatically track state changes and trigger component re-renders | Minimal boilerplate, reactive programming model |
| Computed Values | computed |
Derive values from observables, automatically cache and update | Performance optimization, declarative derived state |
| Actions | action |
Modify observable state, batch updates for performance | Transactional updates, better debugging |
| Reactions | autorun, reaction |
Side effects that run automatically when observables change | Automatic synchronization, no manual subscriptions |
| MobX-State-Tree | types.model() |
Runtime type system with snapshots, patches, time-travel | Type safety, undo/redo, JSON serialization |
Example: MobX Store with Computed Values
import { makeObservable, observable, computed, action } from 'mobx';
import { observer } from 'mobx-react-lite';
class TodoStore {
todos = [];
constructor() {
makeObservable(this, {
todos: observable,
completedCount: computed,
addTodo: action,
toggleTodo: action
});
}
get completedCount() {
// Automatically recomputed when todos change
return this.todos.filter(todo => todo.completed).length;
}
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false });
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
}
const todoStore = new TodoStore();
// Observer component - re-renders only when used observables change
const TodoList = observer(() => {
return (
<div>
<p>Completed: {todoStore.completedCount}/{todoStore.todos.length}</p>
{todoStore.todos.map(todo => (
<div key={todo.id} onClick={() => todoStore.toggleTodo(todo.id)}>
{todo.text}
</div>
))}
</div>
);
});
MobX Philosophy: Anything that can be derived from application state should be derived
automatically. Focus on what to track, not how to track it.
5. Data Fetching Libraries (SWR, Recoil, Apollo)
| Library | Core Hook | Key Feature | Use Case |
|---|---|---|---|
| SWR NEW | useSWR(key, fetcher) |
Stale-While-Revalidate strategy, automatic revalidation on focus/network | Data fetching with smart caching, real-time updates |
| Recoil | atom(), selector() |
Atomic state management with derived state, asynchronous queries | Complex state graphs, React-centric state management |
| SWR Mutations | useSWRMutation() |
Trigger remote mutations with automatic cache updates | POST/PUT/DELETE with optimistic UI updates |
| Recoil Atoms | useRecoilState() |
Independent state units, minimal re-renders, shared across components | Fine-grained state subscriptions, component isolation |
| Recoil Selectors | selector({ get }) |
Pure functions deriving state from atoms, async data fetching | Computed values, GraphQL queries, API integration |
SWR Example
import useSWR from 'swr';
const fetcher = url => fetch(url).then(r => r.json());
function Profile() {
const { data, error, isLoading, mutate } = useSWR(
'/api/user',
fetcher,
{
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 30000 // Poll every 30s
}
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error!</div>;
return (
<div>
<h1>{data.name}</h1>
<button onClick={() => mutate()}>
Refresh
</button>
</div>
);
}
// Optimistic update
import { useSWRConfig } from 'swr';
function UpdateButton() {
const { mutate } = useSWRConfig();
const updateUser = async () => {
// Optimistic update
mutate('/api/user', { name: 'New' }, false);
// Remote update
await fetch('/api/user', { method: 'PATCH' });
// Revalidate
mutate('/api/user');
};
}
Recoil Example
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
// Atom - independent state unit
const textState = atom({
key: 'textState',
default: ''
});
// Selector - derived state
const charCountState = selector({
key: 'charCountState',
get: ({ get }) => {
const text = get(textState);
return text.length;
}
});
// Async selector
const userState = selector({
key: 'userState',
get: async ({ get }) => {
const userId = get(currentUserIdState);
const response = await fetch(`/api/user/${userId}`);
return response.json();
}
});
function TextInput() {
const [text, setText] = useRecoilState(textState);
const count = useRecoilValue(charCountState);
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<p>Character count: {count}</p>
</div>
);
}
SWR vs Recoil Comparison
- SWR: Focused on data fetching, automatic revalidation, 5KB bundle
- Recoil: General state management, atom-based architecture, React-centric
- When to use SWR: API-driven apps, real-time data, caching strategy
- When to use Recoil: Complex state graphs, derived state, async queries
6. TanStack Query (React Query v4, Server State)
| Feature | API | Description | Benefit |
|---|---|---|---|
| useQuery NEW | useQuery({ queryKey, queryFn }) |
Declarative data fetching with automatic caching, background updates | Zero-config caching, deduplication, stale-while-revalidate |
| useMutation | useMutation({ mutationFn }) |
Perform create/update/delete operations with optimistic updates | Automatic invalidation, rollback on error, loading states |
| Query Invalidation | queryClient.invalidateQueries() |
Mark queries as stale to trigger automatic refetch | Keep UI in sync after mutations, smart cache management |
| Parallel Queries | useQueries() |
Execute multiple queries simultaneously, independent loading states | Optimize data fetching, reduce waterfall requests |
| Infinite Queries | useInfiniteQuery() |
Pagination with "load more" pattern, automatic page management | Infinite scroll, cursor-based pagination |
| DevTools | ReactQueryDevtools |
Inspect queries, cache state, refetch manually, time-travel | Debugging cache behavior, performance optimization |
Example: TanStack Query Complete Implementation
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Query
function TodoList() {
const { data, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('/api/todos');
return res.json();
},
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: true,
retry: 3
});
if (isLoading) return <Skeleton />;
if (error) return <Error message={error.message} />;
return <ul>{data.map(todo => <li>{todo.title}</li>)}</ul>;
}
// Mutation with optimistic update
function AddTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newTodo) => fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo)
}),
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
return { previousTodos };
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
return <button onClick={() => mutation.mutate({ title: 'New' })}>Add</button>;
}
// Infinite Query
function InfiniteTodos() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['todos'],
queryFn: ({ pageParam = 0 }) => fetch(`/api/todos?page=${pageParam}`),
getNextPageParam: (lastPage) => lastPage.nextCursor
});
return (
<div>
{data.pages.map(page => page.todos.map(todo => <div>{todo.title}</div>))}
{hasNextPage && (
<button onClick={fetchNextPage} disabled={isFetchingNextPage}>
Load More
</button>
)}
</div>
);
}
Why TanStack Query: Eliminates 90% of server state boilerplate,
automatic background updates, built-in loading/error states, request deduplication, optimistic updates, and
infinite queries out-of-the-box.