Back to React
React
medium
mid

How would you convert an existing piece of UI into a reusable component?

Identify what's variable (data, behavior, presentation) vs fixed (the pattern). Lift variable bits to props: typed data, callbacks for behavior, slot props or `children` / `renderItem` for presentation. Keep the API minimal — fewer props that compose well beat many flags. Style via tokens or `className` passthrough. Document with stories/examples.

4 min read·~15 min to think through

Turning a one-off component into a reusable one is mostly about separating what changes from what doesn't and pushing the change-points into a clean API.

1. Find the seams

Ask of every line: "would another caller want this different?" Three categories:

  • Data — the items / value / current state being rendered.
  • Behavior — what happens on interaction (click, change, submit).
  • Presentation — how an item looks, what wraps it, what icons appear.

Each becomes a prop. Anything else stays internal.

2. Data via typed props

tsx
type Item = { id: string; label: string; disabled?: boolean };
type SelectProps<T extends Item> = {
  items: T[];
  value: T["id"] | null;
  onChange: (id: T["id"]) => void;
  // ...
};

Generic over the item type — callers can extend the shape. Don't over-shape: ask for what you use, no more.

3. Behavior via callbacks

onChange, onSelect, onSubmit — callers own the action. The component is a view + interaction shell.

4. Presentation via slots

The three patterns:

  • children — for a single slot.
  • Render props / renderItem — for repeated slots with item context.
  • Named slots (header, footer) — for multiple distinct slots.
tsx
<List
  items={users}
  renderItem={(u, { isActive }) => <UserRow user={u} active={isActive} />}
/>

This lets callers customize what an item looks like without forking the list.

5. Style via tokens or className passthrough

  • Design tokens (CSS vars) for theming.
  • className passthrough for one-off overrides:
tsx
<Button className={cn("default-styles", className)} {...rest} />
  • asChild / as pattern for swapping the root element (Radix uses this).

Avoid baking in many style props — they multiply faster than the cases they cover.

6. Controlled and uncontrolled

If the component holds internal state, support both controlled and uncontrolled:

tsx
const [value, setValue] = useControlled({ value: props.value, defaultValue: props.defaultValue });

Caller can either pass value + onChange (controlled) or defaultValue (uncontrolled).

7. Composition over configuration

Many flags ("isLarge, isCompact, hasIcon, showDivider") signal too much in one component. Prefer composition:

tsx
<Card>
  <Card.Header>...</Card.Header>
  <Card.Body>...</Card.Body>
  <Card.Footer>...</Card.Footer>
</Card>

vs.

tsx
<Card header={...} body={...} footer={...} showHeader showFooter />

Compound components are more flexible and easier to evolve.

8. Accessibility is part of the API

If the component is interactive, bake in:

  • Keyboard support.
  • Correct ARIA roles.
  • Focus management.
  • Reduced motion.

Reusable doesn't mean "configurable enough to misuse" — the component should be accessible by default.

9. Minimal API

Every prop is a maintenance commitment. Ship the smallest API that covers known callers. Add props when the second caller needs them, not preemptively.

10. Document with examples

Stories (Storybook) or a few code-snippet examples in the docs beat any API table. Show the common cases first; edge cases later.

What to NOT do

  • Hard-code business logic ("if (user.isAdmin) ...") inside a "reusable" component.
  • Pass huge config objects with many flags.
  • Couple to a global store inside a component meant to be reusable.
  • Over-engineer with abstractions for hypothetical future callers.

Interview framing

"Find what varies — data, behavior, presentation — and push each to a prop with the right shape: typed data props, callbacks for behavior, slots (children, renderItem, named slots) for presentation. Style via tokens or className passthrough. Support both controlled and uncontrolled state. Prefer composition (compound components) over many flags. Keep the API minimal — add props when a second caller asks, not preemptively. Bake in accessibility; don't make callers configure their way to it. And don't couple to business logic or a global store — that breaks reuse the moment someone tries to use the component anywhere else."

Follow-up questions

  • renderItem vs children vs compound components — when?
  • How do you support both controlled and uncontrolled in one API?
  • Why is 'add props when needed, not preemptively' the right rule?
  • When is a component NOT worth making reusable?

Common mistakes

  • Hard-coded business logic inside a 'reusable' component.
  • Too many boolean flags.
  • Coupling to a global store.
  • Forcing controlled-only or uncontrolled-only.
  • Preemptive abstractions for hypothetical callers.

Performance considerations

  • Memoize where item lists are large; stable callback identity for memoized children; don't recreate slot functions per render unless needed.

Edge cases

  • Component is only used in one place — don't extract yet.
  • Component depends on a hook/context that callers don't have.
  • Component is used in many places with significant variation — split.

Real-world examples

  • Radix UI, Headless UI, MUI compound components.
  • react-table's flexible render-prop API.

Senior engineer discussion

Seniors keep the API minimal, prefer composition over flags, ship controlled+uncontrolled together, and resist preemptive abstraction. They know the second caller is when reuse begins.

Related questions