Modern State Management Solutions for React Applications
State management is one of the most critical aspects of building scalable React applications. Choosing the right solution can mean the difference between maintainable code and technical debt. Let's explore modern state management patterns and implement them in production scenarios.
The State Management Problem
As React applications grow, passing props through deeply nested components becomes unwieldy. State management solutions provide a way to:
- Share state globally - Avoid prop drilling
- Update state predictably - Clear patterns and data flow
- Debug easily - Time-travel debugging, devtools
- Scale efficiently - Handle complex application state
- Separate concerns - UI state from business logic
Context API
The built-in solution for many use cases.
Basic Setup
class="keyword">class="comment">// contexts/theme-context.tsx
class="keyword">class="string">"use client";
class="keyword">import { createContext, useContext, useState, ReactNode } class="keyword">from class="keyword">class="string">"react";
class="keyword">type Theme = class="keyword">class="string">"light" | class="keyword">class="string">"dark";
class="keyword">interface ThemeContextType {
theme: Theme;
toggleTheme: () => class="keyword">void;
}
class="keyword">class="keyword">const ThemeContext = createContextclass="keyword">undefined>(class="keyword">undefined);
class="keyword">export class="keyword">class="keyword">function ThemeProvider({ children }: { children: ReactNode }) {
class="keyword">class="keyword">const [theme, setTheme] = useState(class="keyword">class="string">"light");
class="keyword">class="keyword">const toggleTheme = () => {
setTheme((prev) => (prev === class="keyword">class="string">"light" ? class="keyword">class="string">"dark" : class="keyword">class="string">"light"));
};
class="keyword">class="keyword">return (
{children}
);
}
class="keyword">export class="keyword">class="keyword">function useTheme() {
class="keyword">class="keyword">const context = useContext(ThemeContext);
class="keyword">class="keyword">if (!context) {
class="keyword">throw class="keyword">new Error(class="keyword">class="string">"useTheme must be used within ThemeProvider");
}
class="keyword">class="keyword">return context;
}
Advanced Context with useReducer
class="keyword">class="comment">// contexts/auth-context.tsx
class="keyword">class="string">"use client";
class="keyword">import { createContext, useContext, useReducer, ReactNode } class="keyword">from class="keyword">class="string">"react";
class="keyword">interface User {
id: string;
email: string;
name: string;
role: class="keyword">class="string">"user" | class="keyword">class="string">"admin";
}
class="keyword">interface AuthState {
user: User | class="keyword">null;
isLoading: boolean;
error: string | class="keyword">null;
}
class="keyword">type AuthAction =
| { class="keyword">type: class="keyword">class="string">"SIGN_IN"; payload: User }
| { class="keyword">type: class="keyword">class="string">"SIGN_OUT" }
| { class="keyword">type: class="keyword">class="string">"SET_LOADING"; payload: boolean }
| { class="keyword">type: class="keyword">class="string">"SET_ERROR"; payload: string | class="keyword">null }
| { class="keyword">type: class="keyword">class="string">"UPDATE_USER"; payload: Partial };
class="keyword">class="keyword">const initialState: AuthState = {
user: class="keyword">null,
isLoading: true,
error: class="keyword">null,
};
class="keyword">class="keyword">function authReducer(state: AuthState, action: AuthAction): AuthState {
class="keyword">switch (action.class="keyword">type) {
class="keyword">case class="keyword">class="string">"SIGN_IN":
class="keyword">class="keyword">return { ...state, user: action.payload, isLoading: false, error: class="keyword">null };
class="keyword">case class="keyword">class="string">"SIGN_OUT":
class="keyword">class="keyword">return { ...state, user: class="keyword">null, isLoading: false };
class="keyword">case class="keyword">class="string">"SET_LOADING":
class="keyword">class="keyword">return { ...state, isLoading: action.payload };
class="keyword">case class="keyword">class="string">"SET_ERROR":
class="keyword">class="keyword">return { ...state, error: action.payload };
class="keyword">case class="keyword">class="string">"UPDATE_USER":
class="keyword">class="keyword">return {
...state,
user: state.user ? { ...state.user, ...action.payload } : class="keyword">null,
};
class="keyword">default:
class="keyword">class="keyword">return state;
}
}
class="keyword">interface AuthContextType class="keyword">extends AuthState {
signIn: (email: string, password: string) => Promise<class="keyword">void>;
signOut: () => Promise<class="keyword">void>;
updateUser: (data: Partial) => Promise<class="keyword">void>;
}
class="keyword">class="keyword">const AuthContext = createContextclass="keyword">undefined>(class="keyword">undefined);
class="keyword">export class="keyword">class="keyword">function AuthProvider({ children }: { children: ReactNode }) {
class="keyword">class="keyword">const [state, dispatch] = useReducer(authReducer, initialState);
class="keyword">class="keyword">const signIn = class="keyword">class="keyword">async (email: string, password: string) => {
dispatch({ class="keyword">type: class="keyword">class="string">"SET_LOADING", payload: true });
class="keyword">try {
class="keyword">class="keyword">const res = class="keyword">class="keyword">await fetch(class="keyword">class="string">"/api/auth/login", {
method: class="keyword">class="string">"POST",
headers: { class="keyword">class="string">"Content-Type": class="keyword">class="string">"application/json" },
body: JSON.stringify({ email, password }),
});
class="keyword">class="keyword">if (!res.ok) class="keyword">throw class="keyword">new Error(class="keyword">class="string">"Login failed");
class="keyword">class="keyword">const user = class="keyword">class="keyword">await res.json();
dispatch({ class="keyword">type: class="keyword">class="string">"SIGN_IN", payload: user });
} class="keyword">catch (error) {
dispatch({
class="keyword">type: class="keyword">class="string">"SET_ERROR",
payload: error class="keyword">instanceof Error ? error.message : class="keyword">class="string">"Unknown error",
});
}
};
class="keyword">class="keyword">const signOut = class="keyword">class="keyword">async () => {
class="keyword">class="keyword">await fetch(class="keyword">class="string">"/api/auth/logout", { method: class="keyword">class="string">"POST" });
dispatch({ class="keyword">type: class="keyword">class="string">"SIGN_OUT" });
};
class="keyword">class="keyword">const updateUser = class="keyword">class="keyword">async (data: Partial) => {
class="keyword">try {
class="keyword">class="keyword">const res = class="keyword">class="keyword">await fetch(class="keyword">class="string">"/api/user", {
method: class="keyword">class="string">"PUT",
headers: { class="keyword">class="string">"Content-Type": class="keyword">class="string">"application/json" },
body: JSON.stringify(data),
});
class="keyword">class="keyword">if (!res.ok) class="keyword">throw class="keyword">new Error(class="keyword">class="string">"Update failed");
dispatch({ class="keyword">type: class="keyword">class="string">"UPDATE_USER", payload: data });
} class="keyword">catch (error) {
dispatch({
class="keyword">type: class="keyword">class="string">"SET_ERROR",
payload: error class="keyword">instanceof Error ? error.message : class="keyword">class="string">"Unknown error",
});
}
};
class="keyword">class="keyword">return (
{children}
);
}
class="keyword">export class="keyword">class="keyword">function useAuth() {
class="keyword">class="keyword">const context = useContext(AuthContext);
class="keyword">class="keyword">if (!context) {
class="keyword">throw class="keyword">new Error(class="keyword">class="string">"useAuth must be used within AuthProvider");
}
class="keyword">class="keyword">return context;
}
Zustand
Lightweight, modern state management library.
Basic Store
class="keyword">class="comment">// store/user-store.ts
class="keyword">import { create } class="keyword">from class="keyword">class="string">"zustand";
class="keyword">import { devtools, persist } class="keyword">from class="keyword">class="string">"zustand/middleware";
class="keyword">interface User {
id: string;
name: string;
email: string;
preferences: {
theme: class="keyword">class="string">"light" | class="keyword">class="string">"dark";
notifications: boolean;
};
}
class="keyword">interface UserState {
user: User | class="keyword">null;
setUser: (user: User) => class="keyword">void;
updatePreferences: (preferences: Partialclass="keyword">class="string">"preferences"]>) => class="keyword">void;
logout: () => class="keyword">void;
}
class="keyword">export class="keyword">class="keyword">const useUserStore = create()(
devtools(
persist(
(set) => ({
user: class="keyword">null,
setUser: (user) => set({ user }),
updatePreferences: (preferences) =>
set((state) => {
class="keyword">class="keyword">if (!state.user) class="keyword">class="keyword">return state;
class="keyword">class="keyword">return {
user: {
...state.user,
preferences: { ...state.user.preferences, ...preferences },
},
};
}),
logout: () => set({ user: class="keyword">null }),
}),
{
name: class="keyword">class="string">"user-storage",
}
)
)
);
Advanced Store with Actions
class="keyword">class="comment">// store/post-store.ts
class="keyword">import { create } class="keyword">from class="keyword">class="string">"zustand";
class="keyword">import { subscribeWithSelector, devtools } class="keyword">from class="keyword">class="string">"zustand/middleware";
class="keyword">interface Post {
id: string;
title: string;
content: string;
published: boolean;
createdAt: Date;
}
class="keyword">interface PostState {
posts: Post[];
selectedPost: Post | class="keyword">null;
isLoading: boolean;
error: string | class="keyword">null;
class="keyword">class="comment">// Actions
fetchPosts: () => Promise<class="keyword">void>;
selectPost: (id: string) => class="keyword">void;
createPost: (post: Omitclass="keyword">class="string">"id" | class="keyword">class="string">"createdAt">) => Promise<class="keyword">void>;
updatePost: (id: string, post: Partial) => Promise<class="keyword">void>;
deletePost: (id: string) => Promise<class="keyword">void>;
publishPost: (id: string) => Promise<class="keyword">void>;
class="keyword">class="comment">// Queries
getPostById: (id: string) => Post | class="keyword">undefined;
getPublishedPosts: () => Post[];
getDraftPosts: () => Post[];
}
class="keyword">export class="keyword">class="keyword">const usePostStore = create()(
subscribeWithSelector(
devtools((set, get) => ({
posts: [],
selectedPost: class="keyword">null,
isLoading: false,
error: class="keyword">null,
fetchPosts: class="keyword">class="keyword">async () => {
set({ isLoading: true, error: class="keyword">null });
class="keyword">try {
class="keyword">class="keyword">const res = class="keyword">class="keyword">await fetch(class="keyword">class="string">"/api/posts");
class="keyword">class="keyword">if (!res.ok) class="keyword">throw class="keyword">new Error(class="keyword">class="string">"Failed to fetch posts");
class="keyword">class="keyword">const posts = class="keyword">class="keyword">await res.json();
set({ posts });
} class="keyword">catch (error) {
set({
error: error class="keyword">instanceof Error ? error.message : class="keyword">class="string">"Unknown error",
});
} class="keyword">finally {
set({ isLoading: false });
}
},
selectPost: (id) => {
class="keyword">class="keyword">const post = get().getPostById(id);
set({ selectedPost: post || class="keyword">null });
},
createPost: class="keyword">class="keyword">async (postData) => {
class="keyword">try {
class="keyword">class="keyword">const res = class="keyword">class="keyword">await fetch(class="keyword">class="string">"/api/posts", {
method: class="keyword">class="string">"POST",
headers: { class="keyword">class="string">"Content-Type": class="keyword">class="string">"application/json" },
body: JSON.stringify(postData),
});
class="keyword">class="keyword">if (!res.ok) class="keyword">throw class="keyword">new Error(class="keyword">class="string">"Failed to create post");
class="keyword">class="keyword">const newPost = class="keyword">class="keyword">await res.json();
set((state) => ({ posts: [...state.posts, newPost] }));
} class="keyword">catch (error) {
set({
error: error class="keyword">instanceof Error ? error.message : class="keyword">class="string">"Unknown error",
});
}
},
updatePost: class="keyword">class="keyword">async (id, updates) => {
class="keyword">try {
class="keyword">class="keyword">const res = class="keyword">class="keyword">await fetch(class="keyword">class="string">/api/posts/${id}, {
method: class="keyword">class="string">"PUT",
headers: { class="keyword">class="string">"Content-Type": class="keyword">class="string">"application/json" },
body: JSON.stringify(updates),
});
class="keyword">class="keyword">if (!res.ok) class="keyword">throw class="keyword">new Error(class="keyword">class="string">"Failed to update post");
class="keyword">class="keyword">const updated = class="keyword">class="keyword">await res.json();
set((state) => ({
posts: state.posts.map((p) => (p.id === id ? updated : p)),
selectedPost:
state.selectedPost?.id === id ? updated : state.selectedPost,
}));
} class="keyword">catch (error) {
set({
error: error class="keyword">instanceof Error ? error.message : class="keyword">class="string">"Unknown error",
});
}
},
deletePost: class="keyword">class="keyword">async (id) => {
class="keyword">try {
class="keyword">class="keyword">const res = class="keyword">class="keyword">await fetch(class="keyword">class="string">/api/posts/${id}, { method: class="keyword">class="string">"class="keyword">DELETE" });
class="keyword">class="keyword">if (!res.ok) class="keyword">throw class="keyword">new Error(class="keyword">class="string">"Failed to delete post");
set((state) => ({
posts: state.posts.filter((p) => p.id !== id),
selectedPost:
state.selectedPost?.id === id ? class="keyword">null : state.selectedPost,
}));
} class="keyword">catch (error) {
set({
error: error class="keyword">instanceof Error ? error.message : class="keyword">class="string">"Unknown error",
});
}
},
publishPost: class="keyword">class="keyword">async (id) => {
class="keyword">class="keyword">await get().updatePost(id, { published: true });
},
getPostById: (id) => get().posts.find((p) => p.id === id),
getPublishedPosts: () => get().posts.filter((p) => p.published),
getDraftPosts: () => get().posts.filter((p) => !p.published),
}))
)
);
class="keyword">class="comment">// Usage in components
class="keyword">export class="keyword">class="keyword">function PostList() {
class="keyword">class="keyword">const posts = usePostStore((state) => state.getPublishedPosts());
class="keyword">class="keyword">const { selectPost } = usePostStore();
class="keyword">class="keyword">return (
{posts.map((post) => (
selectPost(post.id)}>
{post.title}
))}
);
}
TanStack Query (React Query)
Best solution for server state management.
class="keyword">class="comment">// hooks/use-posts.ts
class="keyword">import { useQuery, useMutation, useQueryClient } class="keyword">from class="keyword">class="string">"@tanstack/react-query";
class="keyword">interface Post {
id: string;
title: string;
content: string;
published: boolean;
}
class="keyword">class="keyword">const POSTS_KEY = [class="keyword">class="string">"posts"] as class="keyword">class="keyword">const;
class="keyword">export class="keyword">class="keyword">function usePosts() {
class="keyword">class="keyword">return useQuery({
queryKey: POSTS_KEY,
queryFn: class="keyword">class="keyword">async () => {
class="keyword">class="keyword">const res = class="keyword">class="keyword">await fetch(class="keyword">class="string">"/api/posts");
class="keyword">class="keyword">if (!res.ok) class="keyword">throw class="keyword">new Error(class="keyword">class="string">"Failed to fetch posts");
class="keyword">class="keyword">return res.json();
},
staleTime: 1000 * 60 * 5, class="keyword">class="comment">// 5 minutes
});
}
class="keyword">export class="keyword">class="keyword">function usePost(id: string) {
class="keyword">class="keyword">return useQuery({
queryKey: [...POSTS_KEY, id],
queryFn: class="keyword">class="keyword">async () => {
class="keyword">class="keyword">const res = class="keyword">class="keyword">await fetch(class="keyword">class="string">/api/posts/${id});
class="keyword">class="keyword">if (!res.ok) class="keyword">throw class="keyword">new Error(class="keyword">class="string">"Failed to fetch post");
class="keyword">class="keyword">return res.json();
},
});
}
class="keyword">export class="keyword">class="keyword">function useCreatePost() {
class="keyword">class="keyword">const queryClient = useQueryClient();
class="keyword">class="keyword">return useMutation({
mutationFn: class="keyword">class="keyword">async (post: Omitclass="keyword">class="string">"id">) => {
class="keyword">class="keyword">const res = class="keyword">class="keyword">await fetch(class="keyword">class="string">"/api/posts", {
method: class="keyword">class="string">"POST",
headers: { class="keyword">class="string">"Content-Type": class="keyword">class="string">"application/json" },
body: JSON.stringify(post),
});
class="keyword">class="keyword">if (!res.ok) class="keyword">throw class="keyword">new Error(class="keyword">class="string">"Failed to create post");
class="keyword">class="keyword">return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: POSTS_KEY });
},
});
}
class="keyword">export class="keyword">class="keyword">function useUpdatePost(id: string) {
class="keyword">class="keyword">const queryClient = useQueryClient();
class="keyword">class="keyword">return useMutation({
mutationFn: class="keyword">class="keyword">async (updates: Partial) => {
class="keyword">class="keyword">const res = class="keyword">class="keyword">await fetch(class="keyword">class="string">/api/posts/${id}, {
method: class="keyword">class="string">"PUT",
headers: { class="keyword">class="string">"Content-Type": class="keyword">class="string">"application/json" },
body: JSON.stringify(updates),
});
class="keyword">class="keyword">if (!res.ok) class="keyword">throw class="keyword">new Error(class="keyword">class="string">"Failed to update post");
class="keyword">class="keyword">return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [...POSTS_KEY, id] });
queryClient.invalidateQueries({ queryKey: POSTS_KEY });
},
});
}
class="keyword">class="comment">// Usage in component
(class="keyword">class="string">"use client");
class="keyword">import { usePosts, useCreatePost } class="keyword">from class="keyword">class="string">"@/hooks/use-posts";
class="keyword">export class="keyword">class="keyword">function PostsPage() {
class="keyword">class="keyword">const { data: posts, isLoading, error } = usePosts();
class="keyword">class="keyword">const { mutate: createPost, isPending } = useCreatePost();
class="keyword">class="keyword">if (isLoading) class="keyword">class="keyword">return
Loading...;
class="keyword">class="keyword">if (error) class="keyword">class="keyword">return
Error: {error.message};
class="keyword">class="keyword">return (
Posts
{posts?.map((post: Post) => (
{post.title}
))}
);
}
Redux Toolkit
For complex applications with predictable state flow.
class="keyword">class="comment">// store/features/todos/todosSlice.ts
class="keyword">import { createSlice, createAsyncThunk, PayloadAction } class="keyword">from class="keyword">class="string">"@reduxjs/toolkit";
class="keyword">interface Todo {
id: string;
title: string;
completed: boolean;
}
class="keyword">interface TodosState {
items: Todo[];
isLoading: boolean;
error: string | class="keyword">null;
}
class="keyword">class="keyword">const initialState: TodosState = {
items: [],
isLoading: false,
error: class="keyword">null,
};
class="keyword">export class="keyword">class="keyword">const fetchTodos = createAsyncThunk(
class="keyword">class="string">"todos/fetchTodos",
class="keyword">class="keyword">async (_, { rejectWithValue }) => {
class="keyword">try {
class="keyword">class="keyword">const res = class="keyword">class="keyword">await fetch(class="keyword">class="string">"/api/todos");
class="keyword">class="keyword">if (!res.ok) class="keyword">throw class="keyword">new Error(class="keyword">class="string">"Failed to fetch");
class="keyword">class="keyword">return class="keyword">class="keyword">await res.json();
} class="keyword">catch (error) {
class="keyword">class="keyword">return rejectWithValue(
error class="keyword">instanceof Error ? error.message : class="keyword">class="string">"Unknown error"
);
}
}
);
class="keyword">export class="keyword">class="keyword">const todosSlice = createSlice({
name: class="keyword">class="string">"todos",
initialState,
reducers: {
addTodo: (state, action: PayloadAction) => {
state.items.push(action.payload);
},
toggleTodo: (state, action: PayloadAction) => {
class="keyword">class="keyword">const todo = state.items.find((t) => t.id === action.payload);
class="keyword">class="keyword">if (todo) {
todo.completed = !todo.completed;
}
},
removeTodo: (state, action: PayloadAction) => {
state.items = state.items.filter((t) => t.id !== action.payload);
},
},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.isLoading = true;
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.isLoading = false;
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
},
});
class="keyword">export class="keyword">class="keyword">const { addTodo, toggleTodo, removeTodo } = todosSlice.actions;
class="keyword">export class="keyword">default todosSlice.reducer;
Comparison and When to Use
Context API
✅ Use when:
- Small to medium apps
- Simple theme/language state
- Authentication state
- Global UI state
❌ Avoid when:
- Frequent updates needed
- Complex state logic
- Large applications
- Need time-travel debugging
Zustand
✅ Use when:
- Client-side UI state
- Global preferences
- Modal/sidebar state
- Medium complexity stores
❌ Avoid when:
- Heavy server data fetching
- Complex async flows
TanStack Query
✅ Use when:
- Server state management
- API data fetching/caching
- Background synchronization
- Pagination/infinite queries
❌ Avoid when:
- Pure client-side state
Redux Toolkit
✅ Use when:
- Large enterprise apps
- Complex state dependencies
- Need predictable state management
- Team already familiar with Redux
❌ Avoid when:
- Simple applications
- Over-engineering small features
Best Practices
1. Separate concerns - Keep UI state and server state apart
2. Use TanStack Query for server state - Best-in-class solution
3. Use Zustand for UI state - Simple and lightweight
4. Avoid prop drilling - Use context or state management
5. Memoize selectors - Prevent unnecessary re-renders
6. Normalize state - Store flat structures, not nested
7. Keep reducers pure - No side effects in state logic
8. Test reducers - State mutations are testable
9. Use DevTools - Debug state changes easily
10. Monitor performance - Profile component renders
Conclusion
Modern state management is less about choosing the "best" solution and more about understanding your specific needs:
- Server state → TanStack Query
- UI state → Zustand
- Authentication → Context API
- Complex apps → Redux Toolkit
The key is using the right tool for the right job. Start simple, add complexity only when needed, and always measure performance! 🚀