Practice: unit test a reducer with illegal transitions
Turn "unit test a reducer with illegal transitions" into a concrete interview exercise. Explain the risk, choose the smallest useful test boundary, and describe how the signal prevents regressions.
Answer Strategy
For unit test a reducer with illegal transitions, 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 unit test a reducer with illegal transitions
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.
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
Shows whether you can prove frontend behavior instead of relying on screenshots or manual confidence.
Constraints
- Choose unit, integration, E2E, visual, or performance testing deliberately.
- State the failure that the test catches.
- Avoid brittle assertions that lock implementation details.
Model Answer Shape
- Start with the user-impacting behavior.
- Pick the smallest test that sees that behavior.
- Add one higher-level test only when timing, browser behavior, or integration risk requires it.
Tradeoffs
- Unit tests are fast and precise but cannot prove browser wiring.
- E2E tests are realistic but should be reserved for workflows where integration risk matters.
Edge Cases
- Out-of-order async results.
- Environment-specific browser behavior.
- False confidence from mocks that do not match production contracts.
Testing And Proof
- Regression case for the named risk.
- Negative path or error state.
- Cleanup or retry behavior when relevant.
Follow-Ups
- What would make this test flaky?
- What would you monitor after shipping the fix?