Back to blog

Modern State Management Solutions for React Applications

11 min readBy Mustafa Akkaya
#React#State Management#Zustand#TanStack Query#Redux#Context API

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

app.ts

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

app.ts

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

app.ts

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

app.ts

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.

app.ts

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.

app.ts

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! 🚀