Implement useState derivation in a product component
Use useState derivation to solve a realistic React workflow. Keep rendering, user intent, async synchronization, and error states separate.
Answer Strategy
useState derivation is the question that catches engineers who treat React state as a database. The rule is short: anything you can compute from props plus existing state during render IS NOT state. Mirroring filtered lists, selected items, or formatted strings into useState forces you to sync them with a useEffect, and that sync window is exactly where the stale-data bug lives. The interview win is naming the rule and choosing a derivation site (render, useMemo, or external selector) before any state goes in.
Separate three concerns: identity (what the user picked), visibility (what the current filter shows), and validity (does the picked id still apply to the visible set). Identity is real state. Visibility is a derivation of identity plus props. Validity is a second derivation, computed at render rather than reset from an effect — resetting in an effect causes a frame of wrong selection before the cleanup runs.
Volunteer the danger cases. A useEffect that copies props into state will always run a render late and ship the previous value to children. A useMemo with the wrong dep array will lock onto a snapshot. A selectedId that points at a now-filtered-out row will keep aria-selected="true" on no element. The reference component derives at render and uses memo only as a performance refinement, never as a correctness mechanism.
Reference Implementation: Filter And Selection Derived From Props
A TaskList where the visible slice and the effective selection are both computed from props plus selectedId, with no syncing useEffect.
type Task = { id: string; title: string; tag: 'inbox' | 'today' | 'done' };
type TaskListProps = {
tasks: Task[];
filter: 'all' | 'today' | 'done';
};
// The interview signal: do not mirror props or other state into useState.
// Anything you can compute from props + state during render IS NOT state.
// Storing visibleTasks in useState would go stale every time tasks or filter
// changed without a manual sync effect — the canonical "derived state" bug.
export function TaskList({ tasks, filter }: TaskListProps) {
const [selectedId, setSelectedId] = React.useState<string | null>(null);
// Compute, do not store. Cheap filter on every render is correct and
// simpler than syncing a useEffect. If profiling shows the filter is hot,
// wrap it in useMemo with [tasks, filter] as deps — but never as state.
const visibleTasks = React.useMemo(
() => (filter === 'all' ? tasks : tasks.filter((task) => task.tag === filter)),
[tasks, filter]
);
// Selection survives filter changes only if the selected task is still
// visible. This branch is the senior tell: derive validity at render
// instead of clearing selection from a useEffect, which would flicker.
const effectiveSelectedId = React.useMemo(
() => (visibleTasks.some((task) => task.id === selectedId) ? selectedId : null),
[visibleTasks, selectedId]
);
return (
<ul role="listbox" aria-label="Tasks">
{visibleTasks.map((task) => (
<li
key={task.id}
role="option"
aria-selected={effectiveSelectedId= task.id}
onClick={()=> setSelectedId(task.id)}
>
{task.title}
</li>
))}
</ul>
);
}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.
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
const fixtures: Task[] = [
{ id: 'a', title: 'Inbox A', tag: 'inbox' },
{ id: 'b', title: 'Today B', tag: 'today' },
{ id: 'c', title: 'Done C', tag: 'done' },
];
describe('TaskList', () => {
it('shows the filtered slice without storing it as state', () => {
const { rerender } = render(<TaskList tasks={fixtures} filter="today" />);
expect(screen.queryByText('Inbox A')).toBeNull();
expect(screen.getByText('Today B')).toBeInTheDocument();
// Switching filter must immediately reflect — no stale list, no effect.
rerender(<TaskList tasks={fixtures} filter="done" />);
expect(screen.queryByText('Today B')).toBeNull();
expect(screen.getByText('Done C')).toBeInTheDocument();
});
it('clears effective selection when the selected row leaves the filter', () => {
const { rerender } = render(<TaskList tasks={fixtures} filter="all" />);
fireEvent.click(screen.getByText('Today B'));
expect(screen.getByText('Today B')).toHaveAttribute('aria-selected', 'true');
rerender(<TaskList tasks={fixtures} filter="done" />);
// Done tab no longer contains "Today B"; selection derives to null
// without a separate clear-selection effect.
expect(screen.getByText('Done C')).toHaveAttribute('aria-selected', 'false');
});
});Interviewer Signal
Shows whether you can explain React behavior while building maintainable product UI.
Constraints
- Name what is render-derived and what is stored state.
- Keep side effects owned by events or effects deliberately.
- Provide a testable boundary for business logic.
Model Answer Shape
- Start from the user workflow and state ownership.
- Move pure decisions out of JSX when the branch logic grows.
- Use React primitives to express ownership, not to hide unclear state.
Tradeoffs
- Colocating state improves clarity until sibling coordination becomes the real problem.
- Memoization helps only after render cost or identity churn is measured.
Edge Cases
- Strict Mode re-running development effects.
- Stale closures after async work resolves.
- Unmounts and route changes during in-flight operations.
Testing And Proof
- Reducer or pure function test for core state transitions.
- Interaction test for the user workflow.
- Regression case for stale or repeated async behavior.
Follow-Ups
- How would this change with server rendering?
- Where would you place this state in a larger app?