← Back to question bank
DebuggingSeniorHard#4008 · 35m

Debug useMemo and useCallback boundaries under interview pressure

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

Answer Strategy

For useMemo and useCallback boundaries under interview pressure, do not start with a rewrite. Start with a failing sequence: what the user did, what they saw, what state should have owned the result, and which boundary allowed stale or invalid data through.

Debugging interviews reward evidence. State a hypothesis, inspect the smallest owner boundary, patch that boundary, and leave behind a regression test that would have failed before the fix.

The reference implementation fixes the most common frontend production bug: earlier async work overwriting later user intent after a route change, filter change, or unmount.

Reference Implementation: Regression Fix For useMemo and useCallback boundaries under interview pressure

This pattern applies to search, filters, forms, polling, and UI components where the latest user intent must win over older async work.

type SearchUser = { id: string; name: string };
type SearchState =
  | { tag: 'idle'; users: SearchUser[] }
  | { tag: 'loading'; query: string; users: SearchUser[] }
  | { tag: 'success'; query: string; users: SearchUser[] }
  | { tag: 'error'; query: string; message: string; users: SearchUser[] };

function useUserSearch(query: string) {
  const [state, setState] = React.useState<SearchState>({
    tag: 'idle',
    users: [],
  });

  React.useEffect(() => {
    const normalized = query.trim();
    if (!normalized) {
      setState({ tag: 'idle', users: [] });
      return;
    }

    const controller = new AbortController();
    setState((previous) => ({
      tag: 'loading',
      query: normalized,
      users: previous.users,
    }));

    fetch('/api/users?q=' + encodeURIComponent(normalized), {
      signal: controller.signal,
    })
      .then((response) => {
        if (!response.ok) throw new Error('HTTP ' + response.status);
        return response.json() as Promise<{ users: SearchUser[] }>;
      })
      .then((payload) => {
        if (!controller.signal.aborted) {
          setState({ tag: 'success', query: normalized, users: payload.users });
        }
      })
      .catch((error) => {
        if (controller.signal.aborted) return;
        setState((previous) => ({
          tag: 'error',
          query: normalized,
          message: error instanceof Error ? error.message : 'Unknown error',
          users: previous.users,
        }));
      });

    return () => controller.abort();
  }, [query]);

  return state;
}

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
Write the exact failing path as an interaction test before changing the implementation.
Patch
Fix ownership at the smallest boundary: abort old work, ignore stale results, or move ambiguous state into a reducer.
Prevent
Add a regression test and a production signal such as client errors, abandoned workflow, or retry rate.
test('aborts stale search requests when the query changes', async () => {
  const aborts: string[] = [];
  vi.stubGlobal('fetch', vi.fn((url: string, init?: RequestInit) => {
    init?.signal?.addEventListener('abort', () => aborts.push(url));
    return Promise.resolve({
      ok: true,
      json: () => Promise.resolve({ users: [{ id: 'u1', name: 'Ada' }] }),
    } as Response);
  }));

  const { rerender } = renderHook(({ query }) => useUserSearch(query), {
    initialProps: { query: 'react' },
  });

  rerender({ query: 'vue' });
  expect(aborts.some((url) => url.includes('react'))).toBe(true);
});

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?