Skip to main content
Guides Skills and frameworks React Component Design Interview — Composition, State Lifting, and Rendering Optimization
Skills and frameworks

React Component Design Interview — Composition, State Lifting, and Rendering Optimization

10 min read · April 25, 2026

A practical guide to React component design interviews: how to structure components, decide where state lives, avoid render traps, and explain tradeoffs clearly under interview pressure.

React Component Design Interview — Composition, State Lifting, and Rendering Optimization

A React component design interview is not a trivia round about hooks. The signal interviewers want is whether you can turn a vague UI prompt into a stable component model: good composition, state lifting only when it buys coordination, and rendering optimization that solves real bottlenecks instead of decorating every line with useMemo. If the prompt is “build a dashboard filter,” “design a modal system,” or “create an editable data table,” the winning answer is a sequence of decisions. You show the component boundaries, the data flow, the API surface, the accessibility model, and the places you would measure before optimizing.

This guide is the working playbook for React component design interviews: what to say, what to draw, how to handle state, how to avoid re-render traps, and how to sound senior without over-engineering the exercise.

What the React component design interview is really testing

Most candidates treat the exercise like a coding problem. Strong candidates treat it like a small product architecture problem. The interviewer is usually checking five things:

| Signal | What they listen for | Bad version | |---|---|---| | Component boundaries | You can split UI by responsibility, not by visual boxes only | One giant component with ten booleans | | State ownership | You can explain local, lifted, derived, and server state | Lifting everything to the top “just in case” | | Composition | You create flexible APIs without impossible prop explosions | variant, mode, isCompact, isSpecial, isAdmin everywhere | | Performance judgment | You know what causes re-renders and what is worth memoizing | Blanket React.memo and useCallback | | User experience | You remember keyboard, loading, error, and empty states | Happy-path-only UI |

The fastest way to level up is to narrate the tradeoff before writing code. Say: “I’m going to start with a simple controlled component because the parent needs to coordinate selection with the table. If this were standalone, I’d keep the open/closed and search state local.” That one sentence shows state judgment.

A repeatable design loop for any component prompt

Use this loop before you touch implementation details:

  1. Clarify the behavior. What can the user do? What states exist? Does it need keyboard support, async data, validation, or persistence?
  2. Name the core data model. For a multi-select: options, selected ids, query, highlighted index, loading, error. For a table: rows, columns, sort, filters, edited cell, pending save.
  3. Split components by responsibility. Container fetches or coordinates. Presentational pieces render. Primitive components own accessibility mechanics.
  4. Choose state ownership. Local for UI-only state. Lifted for coordination. Derived for values computed from props/state. External store only when many distant components need the same data.
  5. Define the public API. Controlled vs uncontrolled, required props, render props or slots, event callbacks, escape hatches.
  6. Call out performance risks. Large lists, expensive filtering, unstable callbacks, object props created during render, context updates, and unnecessary parent re-renders.
  7. Close with tests and edge cases. Keyboard navigation, empty states, slow network, error recovery, duplicate labels, screen reader labels, mobile layout.

This loop keeps you from improvising. It also gives the interviewer places to redirect you if they care more about API design than implementation.

Composition over prop explosions

Composition means the parent arranges smaller pieces instead of passing every possible variation as a prop. In interviews, this is the difference between “I can build today’s mockup” and “I can design a component the team can keep using next year.”

A weak modal API often starts like this:

<Modal
  title="Invite teammate"
  showClose
  showFooter
  primaryText="Send invite"
  secondaryText="Cancel"
  danger={false}
  size="medium"
/>

That works until one product wants a custom footer, another wants a destructive action, and another wants a progress stepper. A compositional API gives structure without trapping every variation inside the modal:

<Modal open={open} onOpenChange={setOpen}>
  <Modal.Header>Invite teammate</Modal.Header>
  <Modal.Body>
    <InviteForm onSubmit={sendInvite} />
  </Modal.Body>
  <Modal.Footer>
    <Button variant="secondary" onClick={() => setOpen(false)}>Cancel</Button>
    <Button type="submit" form="invite-form">Send invite</Button>
  </Modal.Footer>
</Modal>

In an interview, explain the tradeoff: composition is more flexible, but it requires conventions. You still need good defaults, clear subcomponent names, focus management in the parent primitive, and a documented order for header/body/footer. Don’t make every component compositional. A tiny status pill can use props. A modal, data table, command palette, wizard, or form builder usually benefits from composition.

A useful sentence: “I’d use props for finite visual variants and composition for content areas where product teams will want to put arbitrary UI.”

State lifting: where the state should live

State lifting is not a religion. It is a coordination tool. The state should live at the lowest component that has all the information needed to update it and all the consumers that must react to it. If only the dropdown needs to know whether it is open, keep isOpen local. If a parent needs to reset the dropdown after submitting a form, expose a controlled prop or event. If table filters drive a URL query string, the filter state probably belongs above the table.

| State | Usually lives | Why | |---|---|---| | Hover, focus ring, open menu | Local component | UI mechanics, no outside coordination | | Search text inside combo box | Local unless parent must persist/query | Avoid noisy parent renders | | Selected option ids | Parent if it affects other UI or submit | Business state, not just presentation | | Filter/sort/pagination | Parent or route state | Coordinates table, URL, API requests | | Derived count or filtered rows | Computed with memo when expensive | Avoid duplicated state bugs | | Server response | Query cache or parent container | Shared loading/error/retry semantics |

The common mistake is duplicating state. If selectedUsers and selectedUserIds both live in state, you have created a sync problem. Store the minimal source of truth, then derive the rest:

const selectedIds = new Set(value);
const selectedOptions = useMemo(
  () => options.filter(option => selectedIds.has(option.id)),
  [options, selectedIds]
);

Even here, mention that new Set(value) creates a new reference each render. If the list is large, memoize the set from value. If the list is small, keep it simple. Senior React design is knowing when the cure costs more than the disease.

Rendering optimization that actually matters

Rendering optimization in a React component design interview should start with measurement and architecture, not micro-optimizing every callback. The biggest wins usually come from reducing the amount of UI that has to update, keeping props stable across expensive children, virtualizing large lists, and moving state closer to where it changes.

Important re-render causes to name:

  • A parent render re-renders children by default.
  • New object, array, and function references can defeat memoized children.
  • Context updates re-render all consumers of that context value.
  • Derived work done during render runs every render unless memoized.
  • Lists without stable keys cause unnecessary remounts and lost state.

The interview-safe performance ladder:

  1. Start simple. Build correct, accessible behavior first.
  2. Localize volatile state. Don’t make the whole page re-render when a text input changes.
  3. Memoize expensive derived values. Filtering thousands of options, grouping rows, calculating aggregates.
  4. Stabilize props into expensive children. useMemo for column definitions; useCallback for handlers passed to memoized rows.
  5. Virtualize long lists. Render only visible rows/options when counts are large.
  6. Split context. Separate theme/session from frequently changing selection state.
  7. Use React Profiler. Confirm the slow component before changing architecture.

Bad answer: “I’ll wrap everything in React.memo.” Better answer: “The expensive part is likely the option list. I’ll keep query state inside the combobox, memoize the filtered options if the list is large, and virtualize once we’re above a few hundred visible options. I won’t memoize every button because that adds complexity without reducing meaningful work.”

Example prompt: design a searchable multi-select

A strong solution starts with a component map:

UserPickerContainer
  └─ MultiSelect
      ├─ MultiSelect.Trigger
      ├─ MultiSelect.Popover
      │   ├─ SearchInput
      │   ├─ OptionList
      │   │   └─ OptionRow
      │   └─ EmptyState
      └─ SelectedChips

Then define state:

  • value: selected ids, controlled by parent because the form needs it.
  • query: local unless search is server-side.
  • isOpen: local by default, optionally controlled if product needs it.
  • highlightedIndex: local keyboard state.
  • options: provided by parent or query hook.
  • loading/error: owned by the data layer or container.

Public API:

<MultiSelect
  options={users}
  value={selectedUserIds}
  onChange={setSelectedUserIds}
  getOptionLabel={user => user.name}
  getOptionValue={user => user.id}
  placeholder="Add teammates"
  disabled={saving}
/>

Explain why getOptionLabel and getOptionValue matter: they make the component reusable across users, teams, tags, or locations without forcing a universal option shape. For more flexibility, add renderOption and renderValue, but don’t start there unless the prompt requires custom rows.

Talk through keyboard behavior: click trigger opens the list, typing filters, arrow keys move highlight, Enter toggles selected item, Escape closes, Backspace removes the last chip when the input is empty. Mention ARIA roles if relevant: combobox/listbox/options, labels, and focus management. You don’t need to recite the spec; you need to show you remember non-mouse users.

Performance note: keep query and highlight local so typing does not re-render the whole page. Memoize filtered options if options are large. Virtualize if the list can grow dramatically. Keep OptionRow memoized only if row rendering is expensive and props are stable.

Controlled vs uncontrolled components

This question appears constantly. A controlled component receives value and emits changes. An uncontrolled component owns its internal value and may expose default value or imperative reset. Controlled is best when the parent needs validation, submission, URL sync, analytics, or cross-component coordination. Uncontrolled is best for simple reusable UI where outside code does not care until submit.

A senior answer often offers both:

function Tabs({ value, defaultValue, onValueChange }) {
  const [internalValue, setInternalValue] = useState(defaultValue);
  const isControlled = value !== undefined;
  const currentValue = isControlled ? value : internalValue;
  const setValue = next => {
    if (!isControlled) setInternalValue(next);
    onValueChange?.(next);
  };
}

Then add the caveat: don’t switch between controlled and uncontrolled after mount. That produces confusing bugs. Document the API and test both modes if the component is part of a design system.

Common traps in React component design interviews

The first trap is designing from the screenshot only. Visual layout matters, but the real design is behavior over time: loading, saving, validation, selection, reset, and errors.

The second trap is global state too early. If the only shared concern is one dropdown, Redux, Zustand, or context makes the design worse. Use external state when multiple distant components need synchronized state or when caching/server state is involved.

The third trap is premature abstraction. A “generic table that can do everything” often becomes impossible to use. Prefer a focused primitive plus composition: DataTable, DataTable.Toolbar, DataTable.Pagination, custom cell renderers.

The fourth trap is ignoring accessibility until the end. For menus, dialogs, tabs, and comboboxes, accessibility is architecture: focus trap, escape behavior, labels, role semantics, and keyboard order affect component boundaries.

The fifth trap is over-optimizing with unstable dependencies. useMemo around a cheap calculation that depends on a freshly created object does nothing useful. Memoization has maintenance cost. Name the bottleneck first.

How to explain your tradeoffs out loud

Use a simple phrase pattern:

  • “I’m keeping this local because no parent needs to coordinate it.”
  • “I’m lifting this because it affects submission and the table below.”
  • “I’m using composition because product teams will need custom content inside this region.”
  • “I’m using props because the variants are finite and documented.”
  • “I would measure before memoizing, but the likely bottleneck is this list.”
  • “I’m deriving this value instead of storing it so it can’t get out of sync.”

This is how you turn implementation into design signal.

Prep checklist for the week before the interview

Practice five prompts: modal system, tabs, data table, multi-select, and editable form. For each, write the component tree, state table, public API, accessibility notes, and performance risks before coding. Then implement one of them in 45 minutes. Review where you lifted too much state, where props got noisy, and where you forgot an edge state.

Create a small library of patterns: controlled/uncontrolled helper, compound components, render prop for custom rows, memoized column definitions, debounced input, and virtualized list boundary. You do not need a giant design system. You need enough patterns to choose deliberately.

The best React component design interview answer is calm and explicit. Start with the user behavior, split responsibilities, put state where it belongs, compose where variation is open-ended, and optimize the render path only where the UI can actually get slow. That is the signal interviewers are trying to hire.