← Back to question bank
DebuggingSeniorHard#201 · 35mFinance specialization

Debug an effect that submits twice in Strict Mode

Move user-intent side effects into event handlers, make submit idempotent, and explain why dependency-array fixes are not enough.

Answer Strategy

For an effect that submits twice in Strict Mode, 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 an effect that submits twice in Strict Mode

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

Use a transfer form or approval flow, not an analytics toy example.

Constraints

  • Keep local, backend, wallet, chain, and user-visible state distinct.
  • Name the product risk before naming the component.
  • Tie the answer back to testing or rollout safety.

Model Answer Shape

  • Move user-intent side effects into event handlers, make submit idempotent, and explain why dependency-array fixes are not enough.
  • Use explicit ownership boundaries for state, data, and user intent.
  • Describe how the UI prevents misleading certainty during pending or failed operations.

Tradeoffs

  • Finance-grade UI should be conservative about certainty and optimistic about continuity.
  • Local state improves recovery but must not pretend to be canonical business truth.

Edge Cases

  • Refresh during pending work.
  • Duplicate user intent.
  • Backend, wallet, and chain disagree temporarily.

Testing And Proof

  • State transition test.
  • Reload recovery scenario.
  • Accessible status and copy review.

Follow-Ups

  • What would you log for support?
  • How would you roll this out behind a flag?

Deep Finance Practice

This item has an authored finance specialization page with the original prompt, solution, and any available runnable harness.

Open legacy practice #201 ->