Choosing the right state management solution is crucial for React applications. Let's compare the most popular options and understand when to use each:
Best for: Small to medium apps, component tree state sharing
// UserContext.js
const UserContext = createContext();
const userReducer = (state, action) => {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload };
default:
return state;
}
};
export const UserProvider = ({ children }) => {
const [state, dispatch] = useReducer(userReducer, {
user: null,
loading: false,
error: null
});
const login = async (credentials) => {
dispatch({ type: 'SET_LOADING', payload: true });
try {
const user = await authAPI.login(credentials);
dispatch({ type: 'SET_USER', payload: user });
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
};
return (
<UserContext.Provider value={{ ...state, login }}>
{children}
</UserContext.Provider>
);
};
// Usage
const Profile = () => {
const { user, loading, login } = useContext(UserContext);
if (loading) return <div>Loading...</div>;
return <div>Welcome, {user?.name}</div>;
};
Best for: Large applications, complex state logic, time-travel debugging
// store/userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const loginUser = createAsyncThunk(
'user/login',
async (credentials, { rejectWithValue }) => {
try {
const response = await authAPI.login(credentials);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data.message);
}
}
);
const userSlice = createSlice({
name: 'user',
initialState: {
user: null,
loading: false,
error: null
},
reducers: {
clearError: (state) => {
state.error = null;
},
logout: (state) => {
state.user = null;
}
},
extraReducers: (builder) => {
builder
.addCase(loginUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.loading = false;
state.user = action.payload;
})
.addCase(loginUser.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
}
});
export const { clearError, logout } = userSlice.actions;
export default userSlice.reducer;
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';
export const store = configureStore({
reducer: {
user: userReducer
}
});
// Usage
import { useSelector, useDispatch } from 'react-redux';
const Profile = () => {
const { user, loading, error } = useSelector(state => state.user);
const dispatch = useDispatch();
const handleLogin = (credentials) => {
dispatch(loginUser(credentials));
};
if (loading) return <div>Loading...</div>;
return <div>Welcome, {user?.name}</div>;
};
Best for: Simple global state, TypeScript-first apps, minimal boilerplate
// stores/userStore.js
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
const useUserStore = create(
devtools(
persist(
(set, get) => ({
user: null,
loading: false,
error: null,
login: async (credentials) => {
set({ loading: true, error: null });
try {
const user = await authAPI.login(credentials);
set({ user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
logout: () => set({ user: null }),
clearError: () => set({ error: null }),
// Computed values
isAuthenticated: () => !!get().user,
}),
{
name: 'user-storage',
partialize: (state) => ({ user: state.user })
}
)
)
);
export default useUserStore;
// Usage
const Profile = () => {
const { user, loading, login, isAuthenticated } = useUserStore();
// Selective subscription for performance
const userOnly = useUserStore(state => state.user);
if (loading) return <div>Loading...</div>;
return <div>Welcome, {user?.name}</div>;
};
| Feature | Context | Redux Toolkit | Zustand |
|---|---|---|---|
| Bundle Size | 0KB | ~8KB | ~2KB |
| Learning Curve | Low | Medium | Low |
| Boilerplate | Medium | Medium | Minimal |
| DevTools | Basic | Excellent | Good |
| TypeScript | Manual | Good | Excellent |
| Persistence | Manual | Plugin | Built-in |
| Performance | Can cause re-renders | Optimized | Optimized |
| Async Actions | Manual | Built-in | Manual |
// Split contexts to prevent unnecessary re-renders
const UserContext = createContext();
const UserActionsContext = createContext();
const UserProvider = ({ children }) => {
const [user, setUser] = useState(null);
const actions = useMemo(() => ({ setUser }), []);
return (
<UserActionsContext.Provider value={actions}>
<UserContext.Provider value={user}>
{children}
</UserContext.Provider>
</UserActionsContext.Provider>
);
};
// Only subscribe to specific state slices
const userName = useUserStore(state => state.user?.name);
const isLoading = useUserStore(state => state.loading);
Choose the solution that best fits your application's complexity and team expertise.