Useful Cheatsheetsusefulcheatsheets.com
React State Management
Chapter 17 · Page 119
Intermediate

React State Management

Manage application state with useState, useReducer, Context, Redux, or Zustand.

TL;DR

  1. 01Colocate state as close as possible to where it is used to avoid unnecessary re-renders.
  2. 02Use React Query or SWR for server state; reserve useState and Zustand for client-only state.
  3. 03Fix Redux performance with configureStore from Redux Toolkit — createStore is deprecated.

Tips

  1. 01Most apps have far less global state than developers think — UI state like hover, focus, and toggles almost always belongs locally, not in a shared store.
  2. 02Use typed hooks (useAppSelector, useAppDispatch) rather than the raw hooks so you get full TypeScript inference without casting at every call site.
  3. 03Start with useState and useContext, only add Redux or Zustand when state becomes too complex to manage.

Warnings

  1. 01Don't over-engineer state management — use the simplest solution that works for your app size.
Notes
Useful Cheatsheetsusefulcheatsheets.com
React State Management
Chapter 17 · Page 120
Intermediate

React State Management

(continued)

Server State vs Client State

  • Server state is data that lives on the server and needs to stay in sync — fetch it with React Query or SWR, not useState.

    import { useQuery } from "@tanstack/react-query";
    
    function UserProfile({ id }: { id: string }) {
      const { data, isLoading, error } = useQuery({
        queryKey: ["user", id],
        queryFn: () => fetch(`/api/users/${id}`).then(r => r.json()),
        staleTime: 60_000 // cache for 1 minute
      });
      if (isLoading) return <p>Loading...</p>;
      if (error) return <p>Error loading user.</p>;
      return <h1>{data.name}</h1>;
    }
    
  • Client state is UI-only data that doesn't need to persist on the server — modal open/closed, selected tab, input value.

    // Client state: lives in the component, no server sync needed
    const [isMenuOpen, setIsMenuOpen] = useState(false);
    const [activeTab, setActiveTab] = useState("overview");
    
  • Use SWR for lightweight data fetching with automatic revalidation on focus and reconnect.

    import useSWR from "swr";
    const fetcher = (url: string) => fetch(url).then(r => r.json());
    
    function Dashboard() {
      const { data } = useSWR("/api/stats", fetcher, { refreshInterval: 30000 });
      return <p>Users: {data?.count}</p>;
    }
    
  • Avoid duplicating server state in useState — let the fetching library own it and read from its cache.

    // Wrong: duplicating server data into local state
    const [user, setUser] = useState(null);
    useEffect(() => { fetch("/api/me").then(r => r.json()).then(setUser); }, []);
    
    // Right: let React Query own the state
    const { data: user } = useQuery({ queryKey: ["me"], queryFn: () => fetch("/api/me").then(r => r.json()) });
    
  • Invalidate cache entries after mutations so the UI stays in sync without manual state updates.

    const queryClient = useQueryClient();
    await updateUser(data);
    queryClient.invalidateQueries({ queryKey: ["user", id] }); // refetches automatically
    
Notes
Useful Cheatsheetsusefulcheatsheets.com
React State Management
Chapter 17 · Page 121
Intermediate

React State Management

(continued)

State Colocation

  • Place state as close as possible to where it is used — don't lift state higher than necessary.

    // Wrong: lifting filter state to the top-level App when only ProductList needs it
    function App() {
      const [filter, setFilter] = useState("all");
      return <ProductList filter={filter} onFilterChange={setFilter} />;
    }
    
    // Right: colocate filter state inside ProductList where it belongs
    function ProductList() {
      const [filter, setFilter] = useState("all");
      // ...no prop drilling needed
    }
    
  • State used only in one component belongs in that component, not in a parent or global store.

  • State used by two sibling components should live in their closest common ancestor (lift state up one level, not all the way up).

    // Both SearchBox and SearchResults need the query — lift to their parent
    function SearchPage() {
      const [query, setQuery] = useState("");
      return (
        <>
          <SearchBox value={query} onChange={setQuery} />
          <SearchResults query={query} />
        </>
      );
    }
    
  • State used across unrelated parts of the app belongs in a global store (Zustand, Context, or Redux).

  • Avoid premature globalization — start local and lift only when a second consumer appears.

    // Progression: local → lifted → global
    // 1. useState in component (local)
    // 2. useState in parent (lifted) — when a sibling needs it
    // 3. Zustand or Context (global) — when distant components need it
    
Notes
Useful Cheatsheetsusefulcheatsheets.com
React State Management
Chapter 17 · Page 122
Intermediate

React State Management

(continued)

State Performance Patterns

  • Use functional state updates when new state depends on previous state to avoid stale closures.

    // Wrong: may read stale count if React batches updates
    setCount(count + 1);
    
    // Right: always uses the latest committed state
    setCount(prev => prev + 1);
    setItems(prev => [...prev, newItem]);
    
  • Batch related state updates with Object.assign or a single state object to avoid extra renders.

    // Two setState calls = two renders
    setLoading(true);
    setError(null);
    
    // One setState call = one render
    setState(prev => ({ ...prev, loading: true, error: null }));
    
  • Use Zustand's selector to subscribe only to the slice of state a component needs.

    const useStore = create((set) => ({
      count: 0, user: null, theme: "light",
      setCount: (n) => set({ count: n }),
    }));
    
    // Only re-renders when count changes, not when user or theme changes
    const count = useStore(state => state.count);
    
  • Derive values during render instead of storing derived state — computed values don't need setState.

    // Wrong: storing derived state
    const [items, setItems] = useState([]);
    const [count, setCount] = useState(0); // always equals items.length
    
    // Right: derive during render
    const [items, setItems] = useState([]);
    const count = items.length; // computed, always accurate, no sync needed
    
  • Use React.memo plus stable callback references to prevent child re-renders from parent state changes.

    const handleClick = useCallback(() => dispatch({ type: "SUBMIT" }), [dispatch]);
    const ExpensiveChild = React.memo(({ onClick }) => <button onClick={onClick}>Submit</button>);
    
Notes
Useful Cheatsheetsusefulcheatsheets.com
React State Management
Chapter 17 · Page 123
Intermediate

React State Management

(continued)

Redux for Large Apps

  • Use Redux Toolkit's configureStorecreateStore is deprecated since Redux 4.x.

    import { configureStore } from "@reduxjs/toolkit";
    import counterReducer from "./counterSlice";
    
    export const store = configureStore({
      reducer: { counter: counterReducer }
    });
    export type RootState = ReturnType<typeof store.getState>;
    export type AppDispatch = typeof store.dispatch;
    
  • Define slices with createSlice to eliminate action type constants and manual spread reducers.

    import { createSlice } from "@reduxjs/toolkit";
    
    const counterSlice = createSlice({
      name: "counter",
      initialState: { value: 0 },
      reducers: {
        increment: state => { state.value += 1; }, // Immer allows direct mutation
        decrement: state => { state.value -= 1; },
        setValue: (state, action) => { state.value = action.payload; }
      }
    });
    export const { increment, decrement, setValue } = counterSlice.actions;
    export default counterSlice.reducer;
    
  • Wrap your app in <Provider store={store}> to make state available to all components.

    import { Provider } from "react-redux";
    root.render(<Provider store={store}><App /></Provider>);
    
  • Read state with useSelector and dispatch actions with useDispatch.

    function Counter() {
      const count = useSelector((state: RootState) => state.counter.value);
      const dispatch = useDispatch<AppDispatch>();
      return <button onClick={() => dispatch(increment())}>{count}</button>;
    }
    
  • Use Redux DevTools to inspect state history, replay actions, and debug time-travel in development.

Notes
Useful Cheatsheetsusefulcheatsheets.com
React State Management
Chapter 17 · Page 124
Intermediate

React State Management

(continued)

Lightweight Alternatives

  • Create a Zustand store with state and actions in a single create call.

    import { create } from "zustand";
    
    const useStore = create<{ count: number; increment: () => void }>((set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
    }));
    
    function Counter() {
      const { count, increment } = useStore();
      return <button onClick={increment}>Count: {count}</button>;
    }
    
  • Use Jotai atoms for fine-grained, component-level shared state without a central store.

    import { atom, useAtom } from "jotai";
    
    const countAtom = atom(0);
    
    function Counter() {
      const [count, setCount] = useAtom(countAtom);
      return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
    }
    
  • Use Recoil atoms and selectors to share state and compute derived values declaratively.

    import { atom, selector } from "recoil";
    
    const textState = atom({ key: "textState", default: "" });
    const charCountState = selector({
      key: "charCountState",
      get: ({ get }) => get(textState).length, // derived from textState
    });
    
  • Access Zustand state anywhere without wrapping the app in a Provider.

    // No <Provider> needed — call the hook directly in any component
    function Navbar() {
      const user = useStore((state) => state.user); // selector for one slice
      return <p>Hello {user?.name}</p>;
    }
    
  • Choose Zustand or Jotai over Redux when you want minimal boilerplate for small to medium apps.

    // Redux: actions + reducer + selectors + Provider setup
    // Zustand: one create() call, one hook, no Provider required
    // Jotai: one atom() call, one useAtom() hook, no Provider required
    const theme = useStore((s) => s.theme); // direct selector access
    
Notes
Useful Cheatsheetsusefulcheatsheets.com
React State Management
Chapter 17 · Page 125
Intermediate

React State Management

(FAQ)

FAQ

Use useReducer when you have multiple related state values that change together, or when next state depends on the previous state in complex ways. If you find yourself writing several useState calls that always update together, that's a signal to consolidate with useReducer.

Context API replaces Redux for many medium-sized apps, but it's not optimized for high-frequency updates — every consumer re-renders when context value changes. For apps with frequent global state updates or large teams needing strict state patterns, Redux or Zustand will perform better.

Zustand has a much smaller API surface and requires no boilerplate — you define a store in a single function call versus Redux's actions, reducers, and selectors. Choose Zustand for smaller teams or projects where Redux's structure feels like overhead.

Any state change in the context value causes all consumers to re-render, even if they only use a part of the value. Fix this by splitting large contexts into smaller focused ones, or memoizing the context value with useMemo.

Yes, and it's often the right call — local UI state in useState, shared server data in React Query or SWR, and global client state in Context or Zustand can coexist cleanly. Mixing solutions by concern keeps each layer simple rather than forcing everything through one system.