React · State Management

State Management Interview Questions
With Answers & Code Examples

8 carefully curated State Management interview questions with working code examples and real interview gotchas.

Practice Interactively →← All Categories
8 questions3 beginner4 core1 advanced
Q1Beginner

What are the different state management options in React and when do you use each?

💡 Hint: Local state → Context → Zustand/Jotai → Redux — choose based on scope and update frequency

Choose based on scope (who needs the state) and update frequency:

  • useState / useReducer — component-local state. First choice always.
  • Lifting state up — share between sibling components via common ancestor.
  • Context + useReducer — low-frequency global state (theme, auth, locale). Don't put rapidly-changing state here.
  • Zustand / Jotai / Recoil — global state with fine-grained subscriptions. Components only re-render when their slice changes.
  • Redux Toolkit — large teams, complex state, need time-travel debugging, strict patterns.
  • React Query / SWR — server state (fetched data). Cache, invalidation, background refresh. NOT the same as UI state.
// Server state — always React Query or SWR, not useState
const { data, loading, error } = useQuery(['user', id], () => fetchUser(id));

// Global UI state (e.g. sidebar open) — Zustand
const useSidebar = create(set => ({
  open: false,
  toggle: () => set(s => ({ open: !s.open })),
}));

// Form state — react-hook-form or local useState
💡 80% of "state management" problems are actually server state problems. React Query eliminates huge amounts of boilerplate (loading, error, refetch, caching) that people used to put in Redux.
Practice this question →
Q2Core

What is server state vs client state and why does the distinction matter?

💡 Hint: Server state lives on the backend (stale, async, shared); client state is local UI state — they need different tools

Two fundamentally different problems often lumped together:

Client state — UI-only data: is the modal open? which tab is selected? filter selections. Lives in the browser, synchronous, you own it completely.

Server state — data fetched from an API: user profile, products, orders. Lives on the server, asynchronous, can be stale, multiple components may need the same data, can change externally.

// ❌ Treating server state like client state
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
  setLoading(true);
  fetch('/api/users')
    .then(r => r.json())
    .then(setUsers)
    .catch(setError)
    .finally(() => setLoading(false));
}, []); // No caching, no refetch, no deduplication

// ✅ React Query handles all of it
const { data: users, isLoading, error } = useQuery({
  queryKey: ['users'],
  queryFn: () => fetch('/api/users').then(r => r.json()),
  staleTime: 5 * 60 * 1000, // cache for 5 minutes
});
💡 Separating these two concerns simplifies your state management enormously. Use React Query/SWR for everything async. Use Zustand/Context for UI state. You likely don't need Redux at all.
Practice this question →
Q3Beginner

What is the difference between server state and client state?

💡 Hint: Server state lives on the server and must be fetched/synchronized; client state is local to the browser and doesn't need network sync

This distinction, popularized by React Query, is fundamental to choosing the right state tool.

Server state (remote state) — data that originates on the server and is shared across users/sessions.

  • Examples: user profile from an API, product list, blog posts.
  • Characteristics: must be fetched asynchronously, can become stale, may be mutated by other users, needs loading/error states.
  • Best managed by: React Query / TanStack Query, SWR, Apollo Client (for GraphQL).

Client state (UI state) — data that exists only in the browser, doesn't need to be persisted to a server.

  • Examples: modal open/close, selected tab, form field values, theme preference.
  • Characteristics: synchronous, always available, only one user cares about it.
  • Best managed by: useState, useReducer, Zustand, Jotai, or URL state.
💡 Most Redux codebases pre-2020 used Redux to manage server state (storing API responses). React Query eliminates this need — Redux becomes unnecessary for most apps once server state is handled properly.
Practice this question →
Q4Core

When would you choose Zustand over Redux Toolkit?

💡 Hint: Zustand is minimal and boilerplate-free; Redux Toolkit shines for large teams needing strict patterns, devtools, and middleware

Both are client state managers, but they have very different ergonomics.

Zustand advantages:

  • Near-zero boilerplate — no actions, reducers, or dispatch. Create a store in ~10 lines.
  • Works outside React (vanilla JS, utilities) — store is a plain JS object.
  • Fine-grained subscriptions — components only re-render when the slice they subscribe to changes.
  • Easy async — just write async functions in the store; no thunks or sagas needed.
const useStore = create((set) => ({
  count: 0,
  increment: () => set((s) => ({ count: s.count + 1 })),
}));

Redux Toolkit advantages:

  • Opinionated structure — good for large teams where consistency matters more than brevity.
  • Redux DevTools — time-travel debugging, action history, state diffs.
  • RTK Query — built-in server state management (competes with React Query).
  • Middleware ecosystem — logging, analytics, undo/redo.

Pick Zustand for: small-to-medium apps, new projects, when you want simplicity. Pick Redux Toolkit for: large existing Redux codebases, teams that need strict patterns and audit trails.

Practice this question →
Q5Core

What is React Query's staleTime vs gcTime (cacheTime) and how do they interact?

💡 Hint: staleTime = how long data is considered fresh; gcTime = how long inactive cache entries are kept before garbage collection

React Query uses two separate timers for different parts of the cache lifecycle.

staleTime — how long a query's data is considered "fresh" after it was last fetched. During this window, React Query won't refetch even if the component remounts or the user refocuses the window.

  • Default: 0 — data is immediately stale, so React Query refetches on every mount/focus.
  • Set to Infinity for data that never changes (static config, user roles).
  • Set to 5 * 60 * 1000 (5 min) for data that changes infrequently.

gcTime (formerly cacheTime) — how long an inactive (no subscribers) query result stays in the cache before being garbage-collected.

  • Default: 5 * 60 * 1000 (5 min).
  • While in cache, navigating back to a page shows the cached data instantly (even if stale) while refetching in the background.
useQuery({
  queryKey: ['user', id],
  queryFn: fetchUser,
  staleTime: 60_000,   // fresh for 1 min — no refetch on focus
  gcTime: 300_000,     // keep in cache 5 min after last subscriber
});

Key interaction: staleTime ≤ gcTime is the natural order. staleTime controls when to refetch; gcTime controls when to forget.

Practice this question →
Q6Core

What is optimistic updating and how do you implement it with React Query?

💡 Hint: Update the UI immediately before the server confirms — rollback on error; use onMutate to snapshot, onError to restore, onSettled to invalidate

Optimistic updating means updating the UI instantly when a user takes an action, without waiting for the server response. If the server returns an error, the UI rolls back.

Why it matters: eliminates perceived latency for common mutations (liking a post, toggling a checkbox, re-ordering a list).

const mutation = useMutation({
  mutationFn: (newTodo) => api.addTodo(newTodo),

  onMutate: async (newTodo) => {
    // 1. Cancel any in-flight refetches (avoid overwriting optimistic update)
    await queryClient.cancelQueries({ queryKey: ['todos'] });

    // 2. Snapshot the current value for rollback
    const previousTodos = queryClient.getQueryData(['todos']);

    // 3. Optimistically update the cache
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);

    return { previousTodos }; // context passed to onError
  },

  onError: (err, newTodo, context) => {
    // 4. Rollback on error
    queryClient.setQueryData(['todos'], context.previousTodos);
  },

  onSettled: () => {
    // 5. Always refetch to sync with server truth
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});
Practice this question →
Q7Beginner

When should you use URL state instead of React state?

💡 Hint: If the state should survive a page refresh, be shareable via a link, or be bookmarkable — put it in the URL

URL state (query params, path params) is the right choice whenever state should be:

  • Shareable — a user can send a link and the recipient sees the same view. Filters, pagination, search terms, selected tabs.
  • Bookmarkable — browser history works correctly; back button returns to previous filter state.
  • Server-renderable — the server can pre-render the correct state without JS. Important for SEO (search results pages).
  • Persistent across refresh — query params survive F5; React state does not.
// ✅ Filters in URL — shareable, bookmarkable
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category') ?? 'all';
const page = Number(searchParams.get('page')) || 1;

// ✅ UI-only state in React — modal open, tooltip hover
const [modalOpen, setModalOpen] = useState(false);

Avoid URL state for: ephemeral UI state (hover, focus, modal open), sensitive data, state that changes many times per second (animation progress).

Practice this question →
Q8Advanced

What are the performance pitfalls of React Context and how do you avoid them?

💡 Hint: Every consumer re-renders when any value in context changes — split contexts by update frequency, memoize values, or use Zustand instead

The problem: All components that call useContext(MyContext) re-render whenever the context value changes — even if the specific part they use didn't change.

// ❌ One context for everything — user + theme + cart
// Any cart update re-renders Header (which only uses user.name)
const { user, cart, theme } = useContext(AppContext);

Fix 1: Split contexts by update frequency

// UserContext changes rarely, CartContext changes often
// Header subscribes to UserContext only — immune to cart updates
const { user } = useContext(UserContext);

Fix 2: Memoize the context value

const value = useMemo(() => ({ user, updateUser }), [user]);
// Without useMemo, a new object is created every render → all consumers re-render

Fix 3: Use a selector (context + useContextSelector)

// use-context-selector library — only re-renders when selected slice changes
const name = useContextSelector(UserContext, (ctx) => ctx.user.name);

Fix 4: Switch to Zustand/Jotai — atomic subscriptions by design; only components subscribed to a changed slice re-render.

Practice this question →

Other React Interview Topics

Rendering Strategies →Core JS →Type System →React Fundamentals →Functions →Microfrontends →Generics →Async JS →Hooks →Objects →Monorepo →Arrays →'this' Keyword →Utility Types →Error Handling →Modern JS →Bundle Optimization →Performance →DOM & Events →Classes & OOP →Caching Strategies →Component Patterns →Advanced Types →Authentication →React Router →Forms →Advanced Patterns →Frontend Security →Concurrent React →Server Components →Testing →Ecosystem →Network Optimization →Core Web Vitals →Browser APIs →

Ready to practice State Management?

Get AI feedback on your answers, predict code output, and fix real bugs.

Start Free Practice →