← Back to question bank
React AppMidMedium#4001 · 40m

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.

Derivation
Re-render with a new filter prop and assert the visible rows update in the same commit. No flush, no effect — the filtered list must be correct on the first paint.
Selection
Click a row, then change the filter so the row leaves the list. Effective selection must be null on the next render. The original selectedId can stay in state; what matters is that the rendered aria-selected reflects the derived value.
Stability
Wrap the filter in useMemo only when profiling shows it is hot. Test that an unrelated re-render (parent state change) does not recompute the memo by passing referentially equal tasks and asserting render output identity if relevant.
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?