← Back to question bank
DebuggingSeniorHard#4016 · 35m

Debug context performance under interview pressure

A React screen using context performance passes simple tests but breaks during repeated interaction. Find the likely root cause, patch it, and describe the longer-term design improvement.

Answer Strategy

The broken pattern in this question is "wrap state and dispatch into one context value". The cost: every consumer re-renders on every state change, even consumers that only need dispatch. The fix is structural — split into two contexts. Dispatch identity is stable for the lifetime of the provider, so dispatch-only consumers wrapped in React.memo never re-render due to state changes.

Locate the boundary by counting consumers per signal. If most consumers only dispatch (action buttons, links), one context for state and one for dispatch is the cheap, no-allocation answer. If consumers want different slices of state, escalate to a selector store (zustand, jotai, useSyncExternalStore with selectors). The single-context-with-memoized-value pattern only helps when consumers all read all the state.

Adjacent traps: passing { state, dispatch } and memoizing it (the memo deps include state, so it changes anyway), wrapping consumers in React.memo without splitting the context (still re-renders because the value identity changed), and using context as a global-state replacement for redux (you will reinvent it badly). The regression test asserts a render counter cap on a memoized dispatch-only consumer.

Regression Fix: Split State And Dispatch Across Two Contexts

The fixed CartProvider exposes state and dispatch as two providers; dispatch identity is stable so dispatch-only consumers stay quiet.

// THE BUG: a single Provider exposed { state, dispatch } as one value.
// Every consumer re-rendered on every state change, even consumers that
// only used dispatch. The fix splits state and dispatch into two
// providers; dispatch identity is stable so dispatch-only consumers
// stay out of the re-render path.

type State = { items: string[] };
type Action = { type: 'add'; item: string } | { type: 'clear' };

const StateContext = React.createContext<State | null>(null);
const DispatchContext = React.createContext<React.Dispatch<Action> | null>(null);

function reducer(state: State, action: Action): State {
  if (action.type === 'add') return { items: [...state.items, action.item] };
  if (action.type === 'clear') return { items: [] };
  return state;
}

export function CartProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = React.useReducer(reducer, { items: [] });
  return (
    <DispatchContext.Provider value={dispatch}>
      <StateContext.Provider value={state}>{children}</StateContext.Provider>
    </DispatchContext.Provider>
  );
}

export function useCartState() {
  const ctx = React.useContext(StateContext);
  if (!ctx) throw new Error('useCartState must be inside CartProvider');
  return ctx;
}

export function useCartDispatch() {
  const ctx = React.useContext(DispatchContext);
  if (!ctx) throw new Error('useCartDispatch must be inside CartProvider');
  return ctx;
}

Testing Strategy

Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.

Reproduce
Render the broken single-context version with a memoized dispatch-only probe. Trigger an action; assert the probe re-renders. The split-context version flattens that counter to 1.
Patch
Split the provider. Test render counters for both consumer flavors: dispatch-only stays flat across state changes, state consumers update on state changes only.
Prevent
Add a code-review checklist: "if a context value contains both state and setters, split it." Pair with a perf budget test that asserts a render counter cap for representative dispatch-only consumers.
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';

describe('Cart context perf regression', () => {
  it('dispatch-only consumers do not re-render when state changes', () => {
    let dispatchProbeRenders = 0;
    const DispatchProbe = React.memo(function DispatchProbe() {
      dispatchProbeRenders++;
      const dispatch = useCartDispatch();
      return (
        <button onClick={()=> dispatch({ type: 'add', item: 'x' })}>Add</button>
      );
    });
    function Display() {
      const { items } = useCartState();
      return <span data-testid="count">{items.length}</span>;
    }

    render(
      <CartProvider>
        <DispatchProbe />
        <Display />
      </CartProvider>
    );
    expect(dispatchProbeRenders).toBe(1);
    fireEvent.click(screen.getByText('Add'));
    expect(screen.getByTestId('count')).toHaveTextContent('1');
    // Dispatch identity is stable across reducer commits, so the memoized
    // probe never re-renders. The broken single-context version would
    // climb to 2 here.
    expect(dispatchProbeRenders).toBe(1);
  });
});

Interviewer Signal

Tests whether you debug from ownership and lifecycle instead of random dependency-array edits.

Constraints

  • State a hypothesis before changing code.
  • Name what evidence would confirm the bug.
  • Avoid broad rewrites unless the current API cannot express the behavior.

Model Answer Shape

  • Reproduce the failing sequence first.
  • Inspect ownership boundaries: local state, props, effects, subscriptions, and server data.
  • Patch the minimal broken boundary and add a regression test.

Tradeoffs

  • A minimal patch reduces risk, but repeated lifecycle bugs often justify a small reducer or custom hook.
  • Adding dependencies can silence lint warnings while still preserving the wrong ownership model.

Edge Cases

  • Double clicks and repeated submissions.
  • Slow network responses arriving out of order.
  • Component remount with stale persisted state.

Testing And Proof

  • Failing interaction sequence.
  • Out-of-order async response.
  • Unmount cleanup.

Follow-Ups

  • What would the code review comment say?
  • What metric or log would show this in production?