DevRoadmap
React

React State Management: Context, Zustand, and When to Use Each

State management is the most debated topic in React development. This guide cuts through the noise with a clear framework for choosing the right tool based on your actual situation.

READ TIME 9 min read
CATEGORY React
Advertisement

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>;
}
Decision FrameworkUse 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.
Advertisement