Start With Local State
The most common mistake in React state management is reaching for a global solution too early. Before adding any library, ask: does this state need to be shared across multiple distant components? If the answer is no, useState in the component that needs it is the right answer — always.
// Local state — perfect for component-specific data
function SearchBar() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
// This state only matters here — no need for global state
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
);
}
Lifting State Up
When two sibling components need the same state, lift it to their nearest common parent. This is simpler than it sounds and handles the majority of "shared state" cases without any library.
// Parent owns the state and passes it down
function ProductPage() {
const [selectedVariant, setSelectedVariant] = useState(null);
return (
<>
<VariantSelector onSelect={setSelectedVariant} />
<PriceDisplay variant={selectedVariant} />
<AddToCartButton variant={selectedVariant} />
</>
);
}
Context API: Avoid Prop Drilling
Context is built into React and perfect for "global" values that many components need to read: current user, theme (dark/light mode), language, feature flags. It's not designed for high-frequency updates (like form state) because every consumer re-renders when context changes.
// Create context
const AuthContext = createContext(null);
// Provider wraps your app (or a subtree)
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = async (credentials) => {
const user = await authAPI.login(credentials);
setUser(user);
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// Consume anywhere in the tree
function NavBar() {
const { user, logout } = useContext(AuthContext);
return user ? <button onClick={logout}>Log out</button> : <LoginLink />;
}
Zustand: Simple Global State
// npm install zustand
import { create } from "zustand";
const useCartStore = create((set, get) => ({
items: [],
total: 0,
addItem: (product) => set(state => ({
items: [...state.items, product],
total: state.total + product.price
})),
removeItem: (id) => set(state => ({
items: state.items.filter(item => item.id !== id),
total: state.items.filter(item => item.id !== id)
.reduce((sum, item) => sum + item.price, 0)
})),
}));
// Use in ANY component — no Provider needed
function Cart() {
const { items, total, removeItem } = useCartStore();
return (/* render cart */);
}
function ProductCard({ product }) {
const addItem = useCartStore(state => state.addItem);
return <button onClick={() => addItem(product)}>Add to Cart</button>;
}
useState for component-local state. Lift state when siblings need to share. Use Context for low-frequency global values (auth, theme). Use Zustand for complex shared state with frequent updates (cart, real-time data). Only reach for Redux if you're on a team that already uses it or you have very complex state with time-travel debugging needs.