How to convert it 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.
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
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.
<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.
classNamepassthrough for one-off overrides:
<Button className={cn("default-styles", className)} {...rest} />asChild/aspattern 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:
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:
<Card>
<Card.Header>...</Card.Header>
<Card.Body>...</Card.Body>
<Card.Footer>...</Card.Footer>
</Card>vs.
<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.