Production engineering guide for state management comparison covering patterns, implementation strategies, and operational best practices.
State management is the act of tracking and synchronizing data across components, user interactions, and external systems in a frontend application. It matters when components need to share data beyond props, when UI updates depend on asynchronous operations, or when the application must respond to complex user workflows—such as form editing, real-time collaboration, or multi-step wizards.
useStateUse useState when state is only used within a single component and does not need to be shared or synchronized across the UI. The component’s state is managed entirely in memory and persists across renders.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <Spinner />;
if (error) return <ErrorBanner message={error} />;
return <div>{user.name}</div>;
}
useState is sufficient when state changes are isolated and do not affect other components.useState does not trigger re-renders when a parent component re-renders, even if the state value hasn’t changed. A child component with useMemo or useCallback may miss updates unless explicitly memoized.When multiple components need the same data, lift state to a common ancestor. This pattern is effective for small to medium apps with predictable data flow.
function App() {
const [cartItems, setCartItems] = useState([]);
return (
<div>
<ProductList items={products} onAddToCart={setCartItems} />
<Cart items={cartItems} />
</div>
);
}
onAddToCart callback is created inline in App, it creates a new function on every render, causing child components to re-render even if cartItems haven’t changed.// This causes unnecessary re-renders in ProductList
const App = () => {
const [cartItems, setCartItems] = useState([]);
// A new function on every render
const handleAddToCart = item => {
setCartItems([...cartItems, item]);
};
return <ProductList onAddToCart={handleAddToCart} />;
};
To fix: wrap the callback in useCallback.
const App = () => {
const [cartItems, setCartItems] = useState([]);
const handleAddToCart = useCallback((item) => {
setCartItems(prev => [...prev, item]);
}, []);
return <ProductList onAddToCart={handleAddToCart} />;
};
useCallback leads to unnecessary re-renders, especially when ProductList is a list of many items. The useMemo optimization in ProductList may be wasted due to the callback’s identity change.useReducerWhen state is shared across deeply nested components and involves multiple actions, use useContext with useReducer for a scalable, maintainable approach.
const CartContext = createContext();
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] };
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(i => i.id !== action.payload.id)
};
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(i =>
i.id === action.payload.id
? { ...i, quantity: action.payload.quantity }
: i
)
};
default:
throw new Error(`Unknown action type: ${action.type}`);
}
}
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
return (
<CartContext.Provider value={{ state, dispatch }}>
{children}
</CartContext.Provider>
);
}
// Usage in any component
function CartButton({ product }) {
const { state, dispatch } = useContext(CartContext);
const handleAdd = () => {
dispatch({ type: 'ADD_ITEM', payload: product });
};
return (
<button onClick={handleAdd}>
Add {product.name} to cart ({state.items.length})
</button>
);
}
useReducer allows grouping related actions and centralizes state logic.dispatch function is not memoized. If CartButton is rendered frequently, a new dispatch function is created on every render, causing CartButton to re-render even when product is unchanged.// This loses memoization of dispatch
function CartButton({ product }) {
const { state, dispatch } = useContext(CartContext);
// dispatch is recreated on every render
const handleAdd = () => {
dispatch({ type: 'ADD_ITEM', payload: product });
};
return <button onClick={handleAdd}>Add {product.name}</button>;
}
Solution: memoize the dispatch function.
function CartButton({ product }) {
const { state, dispatch } = useContext(CartContext);
const handleAdd = useCallback(() => {
dispatch({ type: 'ADD_ITEM', payload: product });
}, [dispatch, product]);
return <button onClick={handleAdd}>Add {product.name}</button>;
}
When state grows beyond a single component tree—e.g., for real-time updates, server-side rendering, or offline persistence—use an external state store.
// Store setup
const store = {
state: { user: null, cart: [] },
listeners: [],
dispatch(action) {
this.state = reducer(this.state, action);
this.listeners.forEach(listener => listener());
},
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
};
// Component usage
function UserCard() {
const [user, setUser] = useState(null);
useEffect(() => {
const handler = () => {
setUser(store.state.user);
};
store.subscribe(handler);
return () => store.unsubscribe(handler);
}, []);
const handleLogout = () => {
store.dispatch({ type: 'LOGOUT' });
};
return (
<div>
<p>Welcome, {user?.name}</p>
<button onClick={handleLogout}>Logout</button>
</div>
);
}
useEffect cleanup is critical.const user$ = new Subject();
// Emit user changes
user$.next({ id: 1, name: 'Alice' });
user$.next({ id: 1, name: 'Bob' });
// Subscribe in a component
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
const subscription = user$.subscribe(setUser);
return () => subscription.unsubscribe();
}, []);
return <div>{user?.name}</div>;
}
Subject is a stream of values that can emit new state at any time.// This component might render null before user data arrives
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
const subscription = user$.subscribe(setUser);
return () => subscription.unsubscribe();
}, []);
return <div>{user?.name}</div>;
}
// Fix: use `startWith` or initialize state from the stream
const user$ = new Subject();
user$.next({ id: 1, name: 'Alice' }); // emitted before subscription
// Solution: use `BehaviorSubject` or `startWith`
const user$ = new BehaviorSubject({ id: 1, name: 'Alice' });
user$.next({ id: 1, name: 'Bob' });
When state must survive page reloads, navigation, or application restarts, persist state in localStorage, sessionStorage, or a database.
function usePersistedState(key, initialValue) {
const [state, setState] = useState(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(state));
}, [key, state]);
return [state, setState];
}
// Usage
function ThemeToggle() {
const [theme, setTheme] = usePersistedState('app-theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Switch to {theme === 'light' ? 'dark' : 'light'} theme
</button>
);
}
localStorage is the most common persistence mechanism.JSON.stringify and JSON.parse round-trip may lose complex data types (e.g., Map, Date, Set, RegExp, Error instances).const user = {
name: 'Alice',
createdAt: new Date('2023-01-01'),
permissions: new Set(['read', 'write']),
settings: { theme: 'dark' }
};
localStorage.setItem('user', JSON.stringify(user));
// createdAt becomes a string, permissions becomes an array, settings is plain object
// On load:
const saved = JSON.parse(localStorage.getItem('user'));
// createdAt is now a string, not a Date object
Solution: use a custom serializer.
function serialize(value) {
return JSON.stringify(value, (key, value) => {
if (value instanceof Date) return { __type: 'Date', value: value.toISOString() };
if (value instanceof Set) return { __type: 'Set', value: Array.from(value) };
return value;
});
}
function deserialize(value) {
return JSON.parse(value, (key, value) => {
if (value && typeof value === 'object' && value.__type === 'Date') {
return new Date(value.value);
}
if (value && typeof value === 'object' && value.__type === 'Set') {
return new Set(value.value);
}
return value;
});
}
useState for UI-level state (e.g., form inputs), useContext + useReducer for app-wide state (e.g., cart, theme), and an external store for real-time or server-synced data (e.g., WebSocket updates).localStorage or a backend API.useReducer update.// Without persistence
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
// This updates the store but does not persist it
const handleAddItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
return (
<CartContext.Provider value={{ state, dispatch }}>
{children}
</CartContext.Provider>
);
}
Fix: use useEffect to persist state.
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(state));
}, [state]);
return (
<CartContext.Provider value={{ state, dispatch }}>
{children}
</CartContext.Provider>
);
}
| When | Strategy | Key Mechanism | Common Pitfall |
|---|---|---|---|
| Small, isolated components | useState | Direct state variable | Missed re-renders from parent updates |
| Shared, flat state | Lifting state up | Props and callbacks | Unnecessary re-renders from new callback instances |
| Complex, nested state | useContext + useReducer | Centralized reducer and context | dispatch recreated on every render |
| Large, reactive state | External store | Event-driven updates | Missed initial values in subscriptions |
| Persistent state | localStorage + useEffect | JSON serialization | Loss of type information across round-trip |
State management is not a one-size-fits-all problem. The best choice emerges from understanding how data flows across the UI, how often it changes, and how long it must persist.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy