Skip to content
GeeksSmith
intermediate 8 min read

State Management Patterns

Modern frontend state management separates concerns into local component state, server cache (TanStack Query), URL state (search params), and lightweight client stores (Zustand) — each optimized for different access patterns.

Why it matters in interviews

Senior/Lead interviews heavily test whether you understand that not all state belongs in a global store. The ability to architect state by access pattern (local vs shared, client vs server, URL-driven) is a top differentiator.

Visual & Interactive Explanation

State Management Decision Matrix

Interactive Comparison

Local State

useState / useReducer

Component
ScopeSingle component
PersistenceNone (re-mount resets)
Re-renderOnly this component

Zustand

Atomic client store

Shared
ScopeCross-component
PersistenceOptional middleware
Re-renderSelector subscribers only

TanStack Query

Server cache

Server
ScopeAPI responses
PersistenceCache with TTL
Re-renderQuery key subscribers

Code Examples & Implementation

Zustand Store with Selectors
import { create } from "zustand";

interface AppStore {
  count: number;
  name: string;
  increment: () => void;
  setName: (name: string) => void;
}

const useAppStore = create<AppStore>((set) => ({
  count: 0,
  name: "Alice",
  increment: () => set((s) => ({ count: s.count + 1 })),
  setName: (name) => set({ name }),
}));

// Only re-renders when count changes
function Counter() {
  const count = useAppStore((s) => s.count);
  return <p>{count}</p>;
}

// Only re-renders when name changes
function NameDisplay() {
  const name = useAppStore((s) => s.name);
  return <p>{name}</p>;
}
TanStack Query — Server Cache with Background Refetch
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

function TodoList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ["todos"],
    queryFn: () => fetch("/api/todos").then((r) => r.json()),
    staleTime: 30_000, // fresh for 30s
    gcTime: 5 * 60_000, // garbage-collect after 5min unused
  });

  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (newTodo: string) =>
      fetch("/api/todos", {
        method: "POST",
        body: JSON.stringify({ title: newTodo }),
      }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["todos"] });
    },
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return <ul>{data.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>;
}
URL State for Shareable Filters
import { useSearchParams } from "next/navigation";

function ProductFilters() {
  const searchParams = useSearchParams();
  const category = searchParams.get("category") || "all";
  const sort = searchParams.get("sort") || "newest";

  // URL: /products?category=shoes&sort=price
  // Shareable, bookmarkable, back-button friendly

  return (
    <div>
      <p>Category: {category}</p>
      <p>Sort: {sort}</p>
    </div>
  );
}

Interview-Ready Answers

Use local state for component-scoped values, TanStack Query for server data caching, URL params for shareable filters, and Zustand for shared client state.

Follow-up Questions & Deep Dives

🎯 Code Output — What Does This Print?

Question 1hard
const ThemeCtx = React.createContext("light");

function App() {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeCtx.Provider value={theme}>
      <Header />
      <MemoizedContent />
      <button onClick={() => setTheme("dark")}>Toggle</button>
    </ThemeCtx.Provider>
  );
}

const MemoizedContent = React.memo(function Content() {
  console.log("Content render");
  return <p>Content</p>;
});

function Header() {
  const theme = useContext(ThemeCtx);
  console.log("Header render:", theme);
  return <h1>{theme}</h1>;
}
// User clicks Toggle. Does MemoizedContent re-render?

Common Mistakes & Anti-Patterns

  • Storing server data in Redux instead of a dedicated cache layer (TanStack Query)
  • Using React Context for high-frequency state changes, causing all consumers to re-render
  • Not using selector functions with Zustand, subscribing to the entire store
  • Duplicating URL-representable state (filters, pagination) in client state instead of search params

Real-World Architectural Scenario

A dashboard has filters (date range, category, status), a data table with pagination, and a user preferences panel. How do you architect the state?

Rate Your Readiness

Rate how comfortably you can explain this in an interview to update your global readiness gauge:

Rate your confidence: