External State Stores
For large apps, libraries like Zustand or Redux manage global state.
As apps grow, some teams move shared state into a dedicated store library. These handle updates efficiently and often avoid re-rendering components that do not use the changed slice of state.
Popular choices
- Zustand — a tiny hook-based store with minimal boilerplate.
- Redux Toolkit — the modern, opinionated Redux for larger apps.
- Jotai / Recoil — atom-based state.
React also provides useSyncExternalStore, the official hook libraries use to subscribe components to an external store safely.
Example
// Example using Zustand (npm install zustand)
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}));
function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>Count: {count}</button>;
}When to use it
- A large e-commerce site migrates from prop drilling to Zustand so the cart state is accessible in the header, product page, and checkout flow without a single context provider.
- A Redux-powered admin panel uses Redux Toolkit slices to manage normalised server data so multiple views can read the same entities without duplicate fetch calls.
- A game uses Zustand's subscribe API to connect React components to a non-React game loop, keeping UI in sync with engine state without wrapping the loop in React.
More examples
Zustand store setup
Creates a Zustand store with items state and three actions — no boilerplate providers or reducers needed.
import { create } from 'zustand';
export const useCartStore = create(set => ({
items: [],
addItem: item => set(state => ({ items: [...state.items, item] })),
removeItem: id => set(state => ({
items: state.items.filter(i => i.id !== id),
})),
clear: () => set({ items: [] }),
}));Read Zustand store in component
Each component subscribes to only the slice of store state it needs, avoiding unnecessary re-renders.
import { useCartStore } from './cartStore';
function CartBadge() {
const count = useCartStore(state => state.items.length);
return <span className="badge">{count}</span>;
}
function AddToCartButton({ product }) {
const addItem = useCartStore(state => state.addItem);
return (
<button onClick={() => addItem(product)}>Add to cart</button>
);
}Redux Toolkit slice
Defines a Redux Toolkit slice with three reducers; RTK's Immer integration allows direct mutation syntax safely.
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => { state.value += 1; },
decrement: state => { state.value -= 1; },
reset: state => { state.value = 0; },
},
});
export const { increment, decrement, reset } = counterSlice.actions;
export default counterSlice.reducer;
Discussion