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
...restin the parameter list: an object with every prop you didn't name.- Spread props
{...obj}in JSX: passes every field ofobjas its own prop or attribute.- Wrapper component
- A component that adds something small around a real element, such as a styled
Buttonaround<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.
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 9602Know 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.
function Button({ variant = "primary", children, ...rest }) {
// rest might be { onClick: f, type: "submit", disabled: true }
return (
<button className={"btn btn-" + variant} {...rest}>
{children}
</button>
);
}04Decide who wins
Attributes apply left to right. Put {...rest} before attributes you must control, or merge values like className yourself.
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
Change a default
In the first example, change the default to
size = 64. Which avatars change size? (Only A and B.) - 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 touchButtonat all. - 3
Fix the careless spread
In the last example, change
Cardtofunction 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
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
Console
Nothing logged yet.Expected console output
A size: 48
B size: 48
C size: null
D size: 0`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
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>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
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.
Break #2
Spread a component-only prop onto the DOM
Pass isActive={true} to a component that spreads all its props onto a <div>.
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 amemochild, 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
hrefto ajavascript:URL, adddangerouslySetInnerHTML, or attach unexpected handlers. React blocksjavascript: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 likeonClikbecome compile errors instead of silent bugs.
Remember this
- 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 passsize, or passessize={undefined}, the component gets 48. The default does not apply tonull,0,falseor"": those are real values the parent chose, and the component receives them as they are. - 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 oldComponent.defaultProps = {...}syntax is removed for function components in React 19; use default parameters instead. - 3
Rest props collect every prop you didn't name:
function Button({ variant, children, ...rest }).restis a new object holding everything else the parent passed, such asonClick,type,disabled,aria-labelortitle. The namerestis just a convention;...propsand...othersare common too. - 4
Spreading props passes all fields of an object as separate props:
<button {...rest}>is the same as writingonClick={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
Order matters. JSX attributes are applied left to right, and later ones win.
<button className="btn" {...rest}>lets aclassNameinrestreplace yours;<button {...rest} className="btn">makes yours win. When both should count, combine them explicitly:className={"btn " + (className ?? "")}after pullingclassNameout ofrest. - 6
When spreading hides bugs. A spread forwards *everything*, including props the element doesn't understand. A component-only prop like
isActiveends up on a<div>and React warns that it doesn't recognise it; a typo likeonClikis 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
How do you give a prop a default value in a function component, and when is it used?
What do ...rest and {...rest} do in a wrapper component?
Why does the position of {...rest} among other attributes matter?
Describe two ways spreading props can hide bugs, and how to avoid them.
Practice
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>.)
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
...restmakes 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
...restand spread them onto an element.I can control which attribute wins by ordering spreads.
I can merge
classNameinstead of overwriting it.I can explain how spreading leaks props and how to filter them.