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 StrategiesCore JSType SystemReact FundamentalsFunctionsMicrofrontendsGenericsAsync JSHooksObjectsMonorepoArrays'this' KeywordUtility TypesError HandlingModern JSBundle OptimizationPerformanceDOM & EventsClasses & OOPCaching StrategiesComponent PatternsAdvanced TypesAuthenticationReact RouterFormsAdvanced PatternsFrontend SecurityConcurrent ReactServer ComponentsTestingEcosystemNetwork OptimizationCore Web VitalsBrowser APIs

Ready to practice State Management?

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

Start Free Practice →