Props Destructuring & Default Props
Destructuring, optional props, default values, rest and spread - and designing a clean component API.
What you will be able to do
- Destructure props in the parameter and in the body
- Mark props optional with ? and give them defaults
- Explain exactly when a default applies - and when it does not
- Rename props, and combine renaming with defaults
- Collect remaining props with rest, and pass an object with spread
- Wrap a native element and forward its attributes with ComponentProps
- Design a props API with the right props required
The idea, in plain English
Props arrive as one object, so everything JavaScript can do with objects works on them. The most useful is destructuring: function CourseCard({ title, level, lessons }: CourseCardProps) pulls the three values out of the props object right in the parameter list, so the body says title instead of props.title.
Destructuring is plain JavaScript, not a React feature - which also means JavaScript default values come with it. level = "Beginner" in the parameter gives level a fallback whenever the caller does not provide one. Paired with an optional prop in the type (level?: string), that is how React components get sensible defaults.
The same syntax covers the rest: title: courseTitle renames, ...rest collects whatever is left, and {...object} at the call site spreads an object into props. Together they let you design components that are easy to call correctly and hard to call wrongly.
Worked example: A CourseCard and a Button with sensible defaults.
Destructuring the props object
const { title, level } = props; creates two variables from the props object. You can do that in the body, or go one step further and destructure in the parameter: function CourseCard({ title, level }: CourseCardProps). The second is what most React code looks like.
The parameter form also documents the component: the first line lists every prop it uses. Keep the type annotation after the braces - the type describes the whole object, not the individual names.
Optional props and defaults
A ? in the type makes a prop optional: level?: string lets callers leave it out. Inside the component its type is then string | undefined, and you have to handle the undefined case.
A default does that for you. function CourseCard({ title, level = "Beginner" }: CourseCardProps) gives level the value "Beginner" whenever the caller does not pass one, and the type inside the component becomes plain string.
Use them together: ? tells callers they may omit the prop, = says what happens when they do. A required prop with a default makes no sense - the default would never be used.
Defaults apply to undefined - nothing else
A destructuring default kicks in only when the value is undefined. Leaving the prop off and passing level={undefined} both give "Beginner".
Everything else is a real value and is used as-is. level="" renders an empty string, not the default. And if the type allows null, level={null} renders nothing - null is not undefined. This matters with API data, where a missing field is often null rather than absent.
When null should also fall back, handle it explicitly: const shown = level ?? "Beginner"; - ?? covers both null and undefined.
<Card />Beginner<Card level={undefined} />Beginner<Card level="" />empty - "" is a value<Card level={null} />empty - null is a value<Card level="Advanced" />AdvancedRenaming: colon, not equals
Inside the braces, = means default and : means rename. { title: courseTitle } reads the title prop into a variable called courseTitle; callers still write title="React".
They combine: { level: courseLevel = "Beginner" } renames and defaults at once. Writing { level: "Beginner" } by mistake is a syntax error - a string cannot be a variable name.
Nested destructuring - use sparingly
Destructuring can reach into nested objects: ({ course: { title, instructor: { name } } }). It is valid, but past one level it gets hard to read, and it throws if a nested object is missing.
Usually clearer: take the object - ({ course }) - and either use course.instructor.name directly or destructure one level in the body. Readable beats short.
Rest and spread
Rest collects: ({ title, ...rest }) puts title in its own variable and every other prop into an object called rest. Spread expands: <CourseCard {...course} /> passes each property of course as a separate prop.
Spread is convenient when an object already matches the props, but it hides what is passed. Extra properties go along silently - spreading a course with an id passes id too - while a missing required property is still a TypeScript error. When a reader needs to see what a component gets, list the props explicitly.
Wrapping native elements
A Button component that only accepts label cannot be disabled, given a type, a className, or an aria-label. Listing all of those by hand does not scale.
Instead, take every native button prop plus your own: type ButtonProps = ComponentProps<"button"> & { label: string }. Destructure your own props and collect the rest - ({ label, ...buttonProps }) - then spread the rest onto the real element: <button {...buttonProps}>{label}</button>. Callers can now pass anything a button accepts, and TypeScript still rejects props a button does not have.
Only spread onto a DOM element what belongs there. If a custom prop like lessons slips into the rest object, React passes it to the DOM and warns "React does not recognize the lessons prop on a DOM element".
Designing the props API
Every prop is a decision. Make a prop required when the component cannot do its job without it - a CourseCard needs a title. Make it optional, with a default, when there is a sensible fallback - level can default to "Beginner". Make it optional without a default when absence means "do not show this" - no rating means no stars.
Making everything optional feels flexible but pushes the problem inside: the component now has to cope with a card that has no title. Required props turn that into a TypeScript error at the call site instead.
Where a prop has a fixed set of values, say so in the type: level: "Beginner" | "Intermediate" | "Advanced". level="Expert" is then a compile error, not a bug report.
Syntax and examples
type CourseCardProps = {
title: string;
level: string;
lessons: number;
};
// 1. props.x
function CourseCardA(props: CourseCardProps) {
return <h2>{props.title} - {props.level} - {props.lessons}</h2>;
}
// 2. Destructure in the body
function CourseCardB(props: CourseCardProps) {
const { title, level, lessons } = props;
return <h2>{title} - {level} - {lessons}</h2>;
}
// 3. Destructure in the parameter - the usual form
function CourseCardC({ title, level, lessons }: CourseCardProps) {
return <h2>{title} - {level} - {lessons}</h2>;
}type CourseCardProps = {
title: string; // required
level?: string; // optional
lessons?: number; // optional
published?: boolean; // optional
};
function CourseCard({
title,
level = "Beginner",
lessons = 0,
published = false,
}: CourseCardProps) {
return (
<article>
<h2>{title}</h2>
<p>{level} · {lessons} lessons</p>
<p>{published ? "Published" : "Draft"}</p>
</article>
);
}
<CourseCard title="React" />;
// React / Beginner · 0 lessons / Draft
<CourseCard title="React" level="Advanced" lessons={192} published />;
// React / Advanced · 192 lessons / Publishedfunction Level({ level = "Beginner" }: { level?: string | null }) {
return <li>[{level}]</li>;
}
<Level />; // [Beginner]
<Level level={undefined} />; // [Beginner]
<Level level="" />; // [] "" is a value
<Level level={null} />; // [] null is a value
<Level level="Advanced" />; // [Advanced]
// To fall back on null as well:
function Level2({ level }: { level?: string | null }) {
return <li>[{level ?? "Beginner"}]</li>;
}type Props = {
title?: string;
level?: string;
};
function CourseCard({
title: courseTitle = "Untitled Course", // rename + default
level: courseLevel, // rename only
}: Props) {
return (
<h2>
{courseTitle} ({courseLevel ?? "Any level"})
</h2>
);
}
// = means default
// : means rename
// { level: "Beginner" } - syntax error, not a defaulttype Course = {
title: string;
instructor: { name: string; experience: number };
};
// Valid, but hard to scan
function CourseCardDeep({
course: {
title,
instructor: { name, experience },
},
}: {
course: Course;
}) {
return <p>{title} by {name}, {experience} years</p>;
}
// Clearer
function CourseCard({ course }: { course: Course }) {
const { title, instructor } = course;
return <p>{title} by {instructor.name}, {instructor.experience} years</p>;
}type CardProps = { title: string; level: string; lessons: number };
function CourseCard({ title, ...details }: CardProps) {
// details is { level: string; lessons: number }
return (
<div>
<h2>{title}</h2>
<p>{details.level} · {details.lessons} lessons</p>
</div>
);
}
const course = { id: 1, title: "React", level: "Beginner", lessons: 192 };
<CourseCard {...course} />;
// Same as title={course.title} level={course.level} lessons={course.lessons}
// - and id={course.id}: extra properties go along silently.
<CourseCard {...{ title: "React", level: "Beginner" }} />;
// TypeScript: Property 'lessons' is missing ... but required in type 'CardProps'.import type { ComponentProps } from "react";
type ButtonProps = ComponentProps<"button"> & {
label: string;
};
function Button({ label, type = "button", ...buttonProps }: ButtonProps) {
return (
<button type={type} {...buttonProps}>
{label}
</button>
);
}
<Button label="Start Learning" />;
<Button label="Submit" type="submit" />;
<Button label="Saving..." disabled className="primary" aria-label="Saving the course" />;
// Renders: <button type="button" disabled="" class="primary" aria-label="Saving the course">
<Button label="Start" typo="x" />;
// TypeScript: Property 'typo' does not exist on type ...function CourseCard({ title, ...rest }: { title: string; lessons: number }) {
return <div {...rest}>{title}</div>; // rest contains lessons
}
<CourseCard title="React" lessons={192} />;
// DOM: <div lessons="192">React</div>
// Console: React does not recognize the lessons prop on a DOM element.type CourseLevel = "Beginner" | "Intermediate" | "Advanced";
type CourseCardProps = {
title: string; // required - a card without a title is a bug
lessons: number; // required - the card always shows it
level?: CourseLevel; // optional - defaults to "Beginner"
rating?: number; // optional - no rating means no stars
description?: string; // optional - shown only when present
};
function CourseCard({ title, lessons, level = "Beginner", rating, description }: CourseCardProps) {
return (
<article>
<h2>{title}</h2>
{description && <p>{description}</p>}
<p>{level} · {lessons} lessons</p>
{rating !== undefined && <p>⭐ {rating}</p>}
</article>
);
}
<CourseCard title="React" lessons={192} level="Expert" />;
// TypeScript: Type '"Expert"' is not assignable to type 'CourseLevel'.Tip: Read a destructured parameter as a checklist: each name is a prop the component uses, each = is what happens when the caller leaves it out.
Watch out: rating && <p>⭐ {rating}</p> prints a stray 0 for a rating of 0 - the Lesson 6 trap again. Compare explicitly: rating !== undefined.
Destructuring syntax
DestructureTake props out of the object.
({ title, lessons }: Props)DefaultUsed when the value is undefined.
({ level = "Beginner" })RenameRead a prop into another name.
({ title: courseTitle })Rename + defaultBoth at once.
({ level: lvl = "Beginner" })RestCollect the remaining props.
({ label, ...rest })SpreadPass an object as props.
<Card {...course} />OptionalCallers may omit it.
level?: string;
Native propsEverything an element accepts.
ComponentProps<"button">
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Give a component level = "Beginner", then pass nothing, undefined, "", null, and "Advanced". Which ones show Beginner?”
“Change ({ title }) to ({ title: courseTitle }) and fix the body. Does the call site change?”
“Spread a course object with an extra id into CourseCard, then remove a required field from it. Which one does TypeScript complain about?”
“Build Button with ComponentProps<"button">, then pass disabled, className, and onClick. Inspect the DOM.”
What usually goes wrong
Inside destructuring braces, : renames. A string after it is a syntax error.
✗ ({ level: "Beginner" }: Props)✓ ({ level = "Beginner" }: Props)Defaults apply only to undefined. Use ?? if null should fall back too.
✗ ({ level = "Beginner" }) // with level={null} from the API✓ const shown = level ?? "Beginner";The name must match the prop. TypeScript flags a name that is not in the props type.
✗ ({ name }: CourseCardProps) // the prop is title✓ ({ title }: CourseCardProps)Custom props in the rest object become unknown DOM attributes, and React warns.
✗ ({ title, ...rest }) => <div {...rest}>✓ ({ title, lessons, ...divProps }) => <div {...divProps}>When every prop is optional, the component must handle every combination of missing data. Require what the component cannot work without.
✗ type Props = { title?: string; lessons?: number; level?: string };✓ type Props = { title: string; lessons: number; level?: CourseLevel };Older tutorials set Component.defaultProps. With the automatic JSX transform that Vite uses, React 19 ignores it on function components - checked here, the prop arrived as undefined. Use destructuring defaults.
✗ CourseCard.defaultProps = { level: "Beginner" };✓ function CourseCard({ level = "Beginner" }: CourseCardProps)It is valid, hard to read, and throws if a middle object is missing.
Best practices
- Destructure props in the parameter so the first line lists what the component uses.
- Pair every default with an optional prop in the type.
- Remember defaults only replace undefined; use ?? for null.
- Require what the component cannot work without.
- Use literal union types for props with a fixed set of values.
- Spread only when the object truly matches, and only native props onto DOM elements.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
Build a UserCard with name (required), role (default "Student"), and active (default true), showing "Role: Student" and "Status: Active" or "Inactive".
Show hintHide hint
Two optional props with defaults.
Show solutionHide solution
type UserCardProps = {
name: string;
role?: string;
active?: boolean;
};
function UserCard({ name, role = "Student", active = true }: UserCardProps) {
return (
<div>
<h2>{name}</h2>
<p>Role: {role}</p>
<p>Status: {active ? "Active" : "Inactive"}</p>
</div>
);
}
<UserCard name="Chandu" />;
// Chandu / Role: Student / Status: ActiveBuild a LessonCard with title (required), duration (default 30), level (default "Beginner", limited to Beginner/Intermediate/Advanced), and completed (default false), showing "✓ Completed" or "○ Not Completed".
Show hintHide hint
A literal union type for level.
Show solutionHide solution
type LessonCardProps = {
title: string;
duration?: number;
level?: "Beginner" | "Intermediate" | "Advanced";
completed?: boolean;
};
function LessonCard({
title,
duration = 30,
level = "Beginner",
completed = false,
}: LessonCardProps) {
return (
<article>
<h3>{title}</h3>
<p>{duration} minutes</p>
<p>{level}</p>
<p>{completed ? "✓ Completed" : "○ Not Completed"}</p>
</article>
);
}
<LessonCard title="JSX Fundamentals" />;
<LessonCard title="Props" duration={45} level="Intermediate" completed />;Predict the output of <Badge count={0} />, <Badge count={null} />, and <Badge /> for function Badge({ count = 5 }: { count?: number | null }) { return <span>{count}</span>; }
Show hintHide hint
Which of those values is undefined?
Show solutionHide solution
<Badge count={0} /> -> <span>0</span> 0 is a value
<Badge count={null} /> -> <span></span> null is a value, and renders nothing
<Badge /> -> <span>5</span> undefined, so the default appliesA course card with a real API
Design and build a CourseCard whose props type makes the right things required, optional, and defaulted.
- Required: title (string), lessons (number).
- Optional with defaults: level ("Beginner", from a literal union), published (false).
- Optional without defaults: rating (show "⭐ 4.8" only when present - including a rating of 0) and description.
- A Start Learning button that is disabled when the course is not published.
- Render three cards: fully specified, title and lessons only, and one with rating={0}.
type CourseLevel = "Beginner" | "Intermediate" | "Advanced";
type CourseCardProps = {
// decide what is required, optional, and defaulted
};
function CourseCard(/* destructure here */) {
// render the card
}Show one solutionHide solution
type CourseLevel = "Beginner" | "Intermediate" | "Advanced";
type CourseCardProps = {
title: string;
lessons: number;
level?: CourseLevel;
published?: boolean;
rating?: number;
description?: string;
};
function CourseCard({
title,
lessons,
level = "Beginner",
published = false,
rating,
description,
}: CourseCardProps) {
return (
<article>
<h2>{title}</h2>
{description && <p>{description}</p>}
<p>{level}</p>
<p>{lessons} Lessons</p>
{rating !== undefined && <p>⭐ {rating}</p>}
<p>{published ? "Published" : "Coming soon"}</p>
<button disabled={!published}>Start Learning</button>
</article>
);
}
function Catalog() {
return (
<>
<CourseCard
title="React"
lessons={192}
level="Beginner"
published
rating={4.8}
description="Learn React from basics to production."
/>
<CourseCard title="Advanced GraphQL" lessons={40} />
<CourseCard title="Docker" lessons={60} rating={0} published />
</>
);
}Key points
- Props are an object, so JavaScript destructuring works on them - it is not a React feature.
- Destructure in the parameter: ({ title, lessons }: Props).
- ? makes a prop optional; = gives it a default.
- Defaults replace undefined only - not null, not "".
- Inside destructuring braces, = is a default and : is a rename.
- ...rest collects remaining props; {...object} spreads an object into props.
- ComponentProps<"button"> plus rest and spread wraps native elements cleanly.
- Require what the component cannot work without; use literal unions for fixed values.
Quick check before you move on
Interview questions
What is props destructuring?
Using JavaScript object destructuring on the props object - usually in the parameter list - so the component uses title instead of props.title.
How do you give a prop a default value?
With a destructuring default, ({ level = "Beginner" }), and mark the prop optional in the type with level?: string. Do not use the old CourseCard.defaultProps = {...}: with the automatic JSX transform that Vite uses, React 19 ignores it on function components, and the prop is simply undefined.
When does a destructuring default apply?
Only when the value is undefined. null, "", 0, and false are real values and are used as-is. Use ?? to fall back on null as well.
What are rest and spread props used for?
Rest collects the props you did not name - often to forward them to a DOM element. Spread passes an object properties as props. Both are common when wrapping native elements with ComponentProps.
Should every prop be optional?
No. Required props turn missing data into a compile error at the call site. Make a prop optional only when the component has a sensible default or when absence means "do not show this".
Why use a literal union type for a prop?
It limits the prop to known values, so an invalid value like level="Expert" is a TypeScript error instead of a runtime bug, and editors can autocomplete the options.
Quiz
- 1.
What does function Card({ title }: Props) do? A. Creates state B. Destructures props C. Creates context D. Creates a hook
- 2.
Which gives level a default? A. { level = "Beginner" } B. { level: "Beginner" } C. { level?: "Beginner" }
- 3.
What does { level: courseLevel } do? A. Sets a default B. Renames the prop C. Makes it optional D. Spreads it
- 4.
What does <CourseCard {...course} /> do? A. Copies the component B. Passes the object properties as props C. Creates state D. Creates a new root
- 5.
function Card({ count = 10 }: Props). What is count when the prop is omitted? A. null B. 0 C. 10 D. An error
- 6.
Does count={null} trigger the default? A. Yes B. No
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned