Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~21 min· topic 4 of 7

Topic 2.4

Default Values, Rest and Spread Props

In one line

Default parameter values give a prop a fallback when the parent leaves it out, ...rest collects every prop you didn't name, and spreading {...rest} onto an element forwards them all at once: handy for wrapper components, but it can quietly pass bad attributes or overwrite your own.

Think of it like this

A sandwich shop's standard order. If you say nothing, you get the default: white bread, medium size. If you say "brown bread", only that changes. And anything extra you ask for ("no onion, extra cheese") the shop passes straight to the kitchen without reading it. Defaults fill in what you didn't say, and rest and spread pass along everything else.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Default value
A fallback used when a prop is missing or undefined: { size = 48 }.
Rest props
...rest in the parameter list: an object with every prop you didn't name.
Spread props
{...obj} in JSX: passes every field of obj as its own prop or attribute.
Wrapper component
A component that adds something small around a real element, such as a styled Button around <button>.
Forwarding
Passing props you received on to a child or element without using them yourself.
defaultProps
An old way to set defaults. Removed for function components in React 19; use default parameters.

Step by step

01Give a prop a default

Most avatars are 48 pixels. Write = 48 in the destructuring pattern so callers can leave size out.

Avatar.jsxwhole filejsx
function Avatar({ name, size = 48 }) {
  return <img src={"/" + name + ".svg"} alt={name} width={size} height={size} />;
}

<Avatar name="asha" />            // size is 48
<Avatar name="ravi" size={96} />  // size is 96

02Know when the default kicks in

Only a missing prop or undefined uses the default. null and 0 are deliberate values and pass straight through. The first live example below prints all four cases.

03Collect the rest

A Button wrapper cares about variant and children. Everything else (onClick, type, disabled, aria-label) belongs to the real <button>. ...rest gathers it.

Button.jsxwhole filejsx
function Button({ variant = "primary", children, ...rest }) {
  // rest might be { onClick: f, type: "submit", disabled: true }
  return (
    <button className={"btn btn-" + variant} {...rest}>
      {children}
    </button>
  );
}
Collect the restdiagram
Rendering diagram…

04Decide who wins

Attributes apply left to right. Put {...rest} before attributes you must control, or merge values like className yourself.

Input.jsxwhole filejsx
function Input({ className = "", ...rest }) {
  // rest cannot overwrite type or the base class
  return <input {...rest} type="text" className={"input " + className} />;
}

05Filter before you spread

Pull out every prop that is only for your component (isActive, variant) so it never reaches the DOM. Never spread an object that came from outside your code, such as an API response.

Try it yourself

  1. 1

    Change a default

    In the first example, change the default to size = 64. Which avatars change size? (Only A and B.)

  2. 2

    Forward one more attribute

    In the Button example, add id="save" to the Save button. Predict the first console line before running. ([ 'onClick', 'id' ].) You didn't have to touch Button at all.

  3. 3

    Fix the careless spread

    In the last example, change Card to function Card({ title, isActive, className = "", ...rest }) and render <div {...rest} className={"card " + className + (isActive ? " active" : "")}>. Predict the new HTML, then check that the warning is gone.

Code & diagrams

When does a default apply? New tab

Missing and `undefined` both get 48. `null` and `0` are real values the parent chose, so the default is skipped (D has size 0 and is invisible).

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

A size: 48
B size: 48
C size: null
D size: 0
A Button wrapper with rest and spread New tab

`Button` names only `variant`, `size` and `children`. Everything else lands in `rest` and is spread onto the real `<button>`, so `onClick`, `type`, `aria-label`, `disabled` and `title` all work without being listed. Click Save to see its handler run.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

primary md [ 'onClick' ]
danger md [ 'type', 'aria-label' ]
ghost sm [ 'disabled', 'title' ]
<button class="btn btn-primary btn-md">Save</button><button class="btn btn-danger btn-md" type="submit" aria-label="Delete this note">Delete</button><button class="btn btn-ghost btn-sm" disabled="" title="Nothing to undo">Undo</button>
Two bugs a careless spread lets in New tab

React warns about `isActive` on a DOM element, and the final HTML has `class="highlight"`: the `card` class (and its border) was silently overwritten. Fix both by pulling `isActive` and `className` out of the props and merging the class yourself.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

React does not recognize the `isActive` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `isactive` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
<div class="highlight" id="plan"><h3>Plan</h3></div>

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Expect a default for null

In the first example, a parent passes size={null} because "no size was chosen" and expects 48.

terminal
$ Console
── what you'll see ──
C size: null

Break #2

Spread a component-only prop onto the DOM

Pass isActive={true} to a component that spreads all its props onto a <div>.

terminal
$ Console
── what you'll see ──
React does not recognize the `isActive` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `isactive` instead. If you accidentally passed it from a parent component, remove it from the DOM element.

Myth vs fact

Myth

defaultProps is the React way to set defaults.

Fact

For function components it was deprecated and is removed in React 19. Use JavaScript default parameters. (Class components still support it.)

Myth

A default replaces any "empty" value.

Fact

Only undefined (or a missing prop). null, 0, false and "" are kept.

Myth

Spreading props is bad practice.

Fact

Forwarding filtered rest props onto the element a wrapper renders is a standard, accessible pattern. Spreading unfiltered or untrusted objects is the problem.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Default values are evaluated on every render: items = [] creates a new array each time the prop is missing. That's fine for display, but if the value is used in effect dependencies or passed to a memo child, it changes identity every render. Hoist it: const EMPTY = []; function List({ items = EMPTY }).

  • ▸

    {...rest} copies only own enumerable properties, and the spread happens on every render. Order is the whole contract: put props callers may override before the spread and props you must control after it, and document which is which in your component's API (Topic 22.2).

  • ▸

    Spreading untrusted data onto elements is a security risk: it can set href to a javascript: URL, add dangerouslySetInnerHTML, or attach unexpected handlers. React blocks javascript: URLs in React 19, but the defence is to never spread objects you didn't build (Topic 21.3).

  • ▸

    In TypeScript, wrapper components type their rest props from the element: React.ComponentProps<"button"> (Phase 12), so typos like onClik become compile errors instead of silent bugs.

Remember this

  1. 1

    A default value is written in the destructuring pattern: function Avatar({ name, size = 48 }). This is plain JavaScript (default parameters, /javascript Phase 4). If the parent doesn't pass size, or passes size={undefined}, the component gets 48. The default does not apply to null, 0, false or "": those are real values the parent chose, and the component receives them as they are.

  2. 2

    Defaults make a component easier to use: callers write only what differs from the usual case (<Button>Save</Button> instead of <Button variant="primary" size="md">). Choose defaults that are the most common, safest choice. The old Component.defaultProps = {...} syntax is removed for function components in React 19; use default parameters instead.

  3. 3

    Rest props collect every prop you didn't name: function Button({ variant, children, ...rest }). rest is a new object holding everything else the parent passed, such as onClick, type, disabled, aria-label or title. The name rest is just a convention; ...props and ...others are common too.

  4. 4

    Spreading props passes all fields of an object as separate props: <button {...rest}> is the same as writing onClick={rest.onClick} type={rest.type} ... by hand. You can also spread into a component: <Profile {...user} /> (Topic 0.3). Together, rest and spread let you build wrapper components that add a little (a class, an icon) and forward the rest to a real HTML element, so every native attribute keeps working without you listing them all.

  5. 5

    Order matters. JSX attributes are applied left to right, and later ones win. <button className="btn" {...rest}> lets a className in rest replace yours; <button {...rest} className="btn"> makes yours win. When both should count, combine them explicitly: className={"btn " + (className ?? "")} after pulling className out of rest.

  6. 6

    When spreading hides bugs. A spread forwards *everything*, including props the element doesn't understand. A component-only prop like isActive ends up on a <div> and React warns that it doesn't recognise it; a typo like onClik is silently forwarded and never fires; and an object from an API or a user ({...data}) can put attributes on your element you never meant to allow (Topic 21.3). Spread only what you've filtered: pull your own props out by name first, and avoid spreading objects you don't control.

Explain it without notes

01

How do you give a prop a default value in a function component, and when is it used?

02

What do ...rest and {...rest} do in a wrapper component?

03

Why does the position of {...rest} among other attributes matter?

04

Describe two ways spreading props can hide bugs, and how to avoid them.

Practice

01

Write a Heading component with a level prop that defaults to 2 and renders <h2>, <h3> and so on. (Hint: a variable like const Tag = "h" + level; can be used as <Tag>.)

02

Write a TextInput wrapper that takes a label prop, renders a <label> around the text and an <input>, and forwards every other prop to the input.

Trade-offs

  • ↔

    Forwarding ...rest makes wrappers flexible and keeps every native attribute working, but the component's API becomes "anything", which is harder to document and easier to misuse. Listing props explicitly is stricter but needs updating whenever a new attribute is needed.

  • ↔

    Defaults make common cases short, but hidden defaults can surprise readers; keep them obvious and few.

Done when you can

  • I can give props default values and know they apply only to undefined.

  • I can collect unnamed props with ...rest and spread them onto an element.

  • I can control which attribute wins by ordering spreads.

  • I can merge className instead of overwriting it.

  • I can explain how spreading leaks props and how to filter them.