State Management Patterns in React

Compare different state management solutions and learn when to use each approach for your React applications.
State management is one of the most challenging aspects of building React applications.
As your application grows, managing state can become increasingly complex.
In this article, we'll compare different state management solutions and explore when to use each approach. .
// useState for simple component state
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
React's built-in useState and useReducer hooks are perfect for managing local component state.
The useState hook is simpler and ideal for independent pieces of state that don't relate to each other.
For more complex state logic, especially when state transitions depend on the previous state, useReducer provides a more structured approach inspired by Redux. .
// useReducer for more complex state logic
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
throw new Error('Unknown action');
}
}
function CounterWithReducer() {
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>
<button onClick={() => dispatch({ type: 'reset' })}>
Reset
</button>
</div>
);
}
Context API is React's solution for sharing state across components without prop drilling.
It's great for global state that doesn't change frequently, such as user authentication status, theme preferences, or UI state.
However, it's not optimized for high-frequency updates, as a context value change causes all components that use that context to re-render. .
// React Context API for sharing state across components
const ThemeContext = React.createContext('light');
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
<Footer />
</ThemeContext.Provider>
);
}
function Header() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<header>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
</header>
);
}
For more complex applications with frequently changing state, external state management libraries often provide better performance and developer experience.
Redux has been the go-to state management library for React for years.
It provides a predictable state container with a clear data flow: actions are dispatched to change state, reducers specify how state changes in response to actions, and the store holds the application state.
Redux is powerful but can be verbose, requiring a lot of boilerplate code.
Redux Toolkit simplifies this by providing utilities to make common Redux patterns easier to write. .
// Modern Redux with Redux Toolkit
import { createSlice, configureStore } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
incremented: state => {
state.value += 1;
},
decremented: state => {
state.value -= 1;
},
reset: state => {
state.value = 0;
}
}
});
const { incremented, decremented, reset } = counterSlice.actions;
const store = configureStore({
reducer: {
counter: counterSlice.reducer
}
});
// Component that uses Redux
function ReduxCounter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(incremented())}>
Increment
</button>
<button onClick={() => dispatch(decremented())}>
Decrement
</button>
<button onClick={() => dispatch(reset())}>
Reset
</button>
</div>
);
}
Zustand is a more modern alternative that provides a simpler API with less boilerplate.
It uses hooks for accessing state and doesn't require wrapping your app in a provider.
Zustand is lightweight, easy to learn, and performs well even with frequent state updates. .
// State management with Zustand
import create from 'zustand';
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}));
function ZustandCounter() {
const { count, increment, decrement, reset } = useStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
}
So, which solution should you choose? For simple applications or components with minimal state needs, React's built-in hooks are usually sufficient.
When you need to share state across many components, consider Context API for infrequently changing state or a state management library for more dynamic state.
For applications with complex data requirements and frequent server interactions, combine a state management solution with a query library.
Redux or Zustand can handle client-side state, while React Query or SWR manages server-side state.
Remember that you don't have to pick just one solution.
Different parts of your application might benefit from different state management approaches.
The key is to understand the trade-offs and choose the right tool for each specific state management need..
About the Author

Michael Torres
A passionate writer and developer who loves sharing knowledge about web technologies.