skip to content

A reusable Button in a shared component library spreads every unrecognized prop onto the underlying <button> with {...rest}. What risks does that create, and how would you control it?

level: seniorimportance: should knowfreq 40%

answer

  1. later prop wins, so order is policy
  2. every owned prop must be destructured out
  3. className replaces unless you merge it
  4. type the rest as the element's own props

basics

~20 s

Blind spreading leaks unknown props into the DOM, lets callers silently override the component's own attributes depending on spread order, and hides the real API from readers and types. Control it by destructuring what you own, spreading the rest first, and merging className and handlers deliberately.

solid answer

~50 s

Spreading is a good default for a leaf component that wraps a DOM element, because it keeps `aria-*`, `data-*`, `id` and native handlers working without enumerating them. The risks are three. First, position decides who wins: `<button {...rest} type="button" />` makes your value authoritative, while `<button type="button" {...rest} />` lets any caller override it, including `onClick` and `className`. Second, props that were meant for you leak into the DOM — an unrecognized camelCase prop like `isActive` reaches the element and React warns about it in development. Third, the component's real contract disappears: nobody can read the file and know what it accepts. The controls are to destructure every prop you own so it cannot reach `rest`, put `{...rest}` first and your non-negotiable attributes after it, merge `className` explicitly rather than letting it be replaced, and type `rest` as the element's own props so TypeScript rejects nonsense.

code

typescript · 19 lines
typescript
type ButtonProps = React.ComponentPropsWithoutRef<'button'> & {
  variant?: 'primary' | 'ghost';
};

function Button({ variant = 'primary', className, onClick, ...rest }: ButtonProps) {
  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    // component-owned behaviour runs first
    onClick?.(event);
  };

  return (
    <button
      {...rest}
      onClick={handleClick}
      className={`btn btn-${variant} ${className ?? ''}`}
      type={rest.type ?? 'button'}
    />
  );
}

go deeper

for a junior

Know that {...rest} forwards leftover props to the element and that a later prop of the same name wins. Be able to read a component and say which props reach the DOM.

for a middle

Explain the mechanics of ordering, why an unconsumed custom prop triggers a React warning on a DOM element, and why className needs explicit merging rather than being left to the spread.

for a senior

Show design judgment for a shared library: which attributes you make non-negotiable, how handlers are composed rather than replaced, and how typing rest as the element's props restores a readable contract.

for a principal

Own the convention across a design system — where spreading is permitted at all, how override escape hatches are granted without letting consumers break accessibility, and what that policy costs when component internals change.

## Why spreading exists A component that wraps a native element has an unbounded surface. Callers legitimately need `id`, `title`, `aria-label`, `aria-describedby`, `data-testid`, `tabIndex`, `onFocus`, `onKeyDown` and dozens more. Enumerating them turns a fifteen-line component into a hundred-line pass-through list that is always missing the one attribute today's caller needs. So the idiom is to destructure the props you actually own and forward everything else: ```javascript function Button({ variant = 'primary', size = 'medium', className, ...rest }) { return ( <button {...rest} className={`btn btn-${variant} btn-${size} ${className ?? ''}`} type={rest.type ?? 'button'} /> ); } ``` That is a reasonable default for a leaf component. The problems appear when spreading is used without deciding what it means. ## Risk one: order decides authority JSX props are applied left to right and later wins, exactly like object spread. This is not a stylistic detail — it is the whole safety model. ```javascript <button type="button" {...rest} /> // caller can turn this into type="submit" <button {...rest} type="button" /> // your value is final ``` Decide per attribute. Things that define the component's identity and correctness — a role, a controlled `type`, an `aria-` attribute the component computes — belong **after** the spread. Things you are merely providing a sensible default for belong **before** it. The dangerous case is `onClick`: with the spread last, a caller's `onClick` silently replaces your internal handler and the component's behaviour vanishes with no error anywhere. If both must run, compose them explicitly rather than letting one win: ```javascript function Button({ onClick, ...rest }) { const handleClick = (event) => { track('button_click'); onClick?.(event); }; return <button {...rest} onClick={handleClick} />; } ``` ## Risk two: props leaking into the DOM Everything left in `rest` goes to the element. If a caller passes a prop you intended to consume — or a prop that was meant for a different component in the tree — it reaches the DOM. React warns in development about unrecognized camelCase props on DOM elements, and the attribute can end up in the rendered HTML where it does nothing except confuse anyone reading the markup. The fix is discipline in the destructuring: every prop the component owns must be named in the parameter list so it is removed from `rest` by construction. That is also why destructuring defaults are better than a static defaults object — the name is consumed, not merely defaulted. The pathological version is a component that spreads onto a child *component* rather than a DOM element. `<Inner {...rest} />` two or three levels deep produces a tree where no file states what any component accepts, and a typo in a prop name fails silently at every level. ## Risk three: className and style are replaced, not merged A spread `className` overwrites yours completely, so the caller who adds one utility class destroys every base class the component depended on. Because this is the single most common override, handle it explicitly: destructure `className` out of the props, and concatenate it after your own classes so it wins specificity ties without erasing the base. The same applies to `style` — merge the objects rather than letting one replace the other. ## Risk four: the API becomes invisible A library component whose props are `{...rest}` has no readable contract. Consumers guess, autocomplete offers nothing useful, and reviewers cannot tell an intentional prop from a typo. TypeScript answers this without giving up the ergonomics: declare your own props and extend the element's: ```typescript type ButtonProps = React.ComponentPropsWithoutRef<'button'> & { variant?: 'primary' | 'ghost'; }; function Button({ variant = 'primary', className, ...rest }: ButtonProps) { return <button {...rest} className={`btn btn-${variant} ${className ?? ''}`} />; } ``` Now `rest` is exactly the set of real button attributes, an unknown prop is a compile error, and the component's own additions are documented in one place. ## The judgment an interviewer is listening for Spread at the boundary where your component meets the DOM, and be explicit everywhere else. Own the attributes that make the component correct by placing them after the spread. Consume every prop you handle so it cannot leak. Merge the two props callers always override — `className` and `style` — and compose handlers rather than replacing them. Type the rest so the invisible surface is at least checked. A candidate who says only "spreading is bad practice" has missed that it is the standard way real design systems keep native attributes working; a candidate who says only "just spread everything" has never debugged a caller who accidentally turned a button into a submit.

  • Where exactly do you place {...rest} relative to attributes you consider non-negotiable?
    Non-negotiable attributes go after the spread, so your value is applied last and wins. Attributes where you are only supplying a sensible default go before it, so a caller can override. Making that a per-attribute decision — rather than always putting the spread first or last — is the point; it is how the component states which parts of its contract are open.
  • What goes wrong when a component spreads rest onto another component rather than a DOM element?
    The contract disappears in both directions. Nothing in the file says what the inner component accepts, so a mistyped prop is silently forwarded and dropped with no warning, and refactoring the inner component's props breaks callers with no compile error. Spread at the DOM boundary; pass named props between your own components.
  • How do you let a caller add a class without destroying the component's base styling?
    Destructure className out of the props so the spread cannot overwrite yours, then concatenate the caller's value after your base classes on the element. That keeps the base styles and gives the caller's class the later position for specificity ties. The same treatment applies to style, merged as an object rather than replaced.
  • A caller passes onClick and the component's own internal click behaviour stops working. What happened?
    The spread applied the caller's onClick after the component's, so the later prop replaced it — only one handler survives. Destructure onClick out and call it from your own handler, so both run and the order is explicit. Silent handler replacement is the most damaging form of the spread-ordering bug because nothing warns.

saying these in an interview costs you the question

  • Saying prop spreading is always bad practice
  • Assuming spread props merge with same-named props
  • Forwarding unconsumed custom props onto DOM elements
  • Letting a caller's className replace the base classes
  • Spreading rest through several layers of custom components

context