
State Management in React: A Complete Guide
State management is a crucial aspect of React applications. Let's explore different approaches to managing state, from simple local state to complex global state management solutions.
Local State Management
1. useState Hook
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
2. useReducer Hook
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
Context API
1. Basic Context Setup
const ThemeContext = React.createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
2. Context with useReducer
const initialState = {
theme: 'light',
user: null,
notifications: []
};
function reducer(state, action) {
switch (action.type) {
case 'SET_THEME':
return { ...state, theme: action.payload };
case 'SET_USER':
return { ...state, user: action.payload };
case 'ADD_NOTIFICATION':
return {
...state,
notifications: [...state.notifications, action.payload]
};
default:
return state;
}
}
const AppContext = React.createContext();
function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}
Redux
1. Basic Redux Setup
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer
}
});
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => {
state.value += 1;
},
decrement: state => {
state.value -= 1;
}
}
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
2. Redux with Async Actions
// userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchUser = createAsyncThunk(
'user/fetchUser',
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
);
const userSlice = createSlice({
name: 'user',
initialState: {
data: null,
status: 'idle',
error: null
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.status = 'succeeded';
state.data = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
}
});
Zustand
1. Basic Store
import create from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 }))
}));
function Counter() {
const { count, increment, decrement } = useStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
2. Complex Store with Middleware
import create from 'zustand';
import { persist } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
theme: 'light',
toggleTheme: () => set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light'
}))
}),
{
name: 'app-storage',
getStorage: () => localStorage
}
)
);
Jotai
1. Atomic State Management
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
const doubleAtom = atom((get) => get(countAtom) * 2);
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubled] = useAtom(doubleAtom);
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
2. Async Atoms
import { atom, useAtom } from 'jotai';
import { atomWithQuery } from 'jotai/query';
const userAtom = atomWithQuery((get) => ({
queryKey: ['user', get(userIdAtom)],
queryFn: async ({ queryKey: [, id] }) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
}));
Best Practices
- Choose the right state management solution based on your needs
- Keep state as local as possible
- Use context for theme, authentication, and other global settings
- Consider using Redux for complex applications with many state updates
- Use Zustand or Jotai for simpler applications
- Implement proper error handling and loading states
- Use TypeScript for better type safety
- Implement proper testing for state management
- Consider performance implications
- Use devtools for debugging
Conclusion
State management in React has evolved significantly over the years, offering various solutions for different use cases. Whether you're building a small application or a large-scale project, there's a state management solution that fits your needs. Remember to choose the right tool for your specific use case and follow best practices to maintain a clean and maintainable codebase.