← Back to React
Lesson 13 · Components and Props

Props Destructuring & Default Props

Destructuring, optional props, default values, rest and spread - and designing a clean component API.

Beginner35 min

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.

What level = "Beginner" produces - checked in React 19
<Card />Beginner
<Card level={undefined} />Beginner
<Card level="" />empty - "" is a value
<Card level={null} />empty - null is a value
<Card level="Advanced" />Advanced

Renaming: 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

Three ways to read the same props
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>; }
Optional props with defaults
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 / Published
When the default applies - checked in React 19
function 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>; }
Rename, and rename with a default
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 default
Nested destructuring, and the clearer alternative
type 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>; }
Rest and spread
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'.
A Button that accepts every native button prop
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 ...
Unknown props leak onto the DOM
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.
A props type that says what it means
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

Destructure

Take props out of the object.

({ title, lessons }: Props)
Default

Used when the value is undefined.

({ level = "Beginner" })
Rename

Read a prop into another name.

({ title: courseTitle })
Rename + default

Both at once.

({ level: lvl = "Beginner" })
Rest

Collect the remaining props.

({ label, ...rest })
Spread

Pass an object as props.

<Card {...course} />
Optional

Callers may omit it.

level?: string;
Native props

Everything an element accepts.

ComponentProps<"button">

Try it yourself

The code does not change. Swap the content string and the program does something else entirely.

Test the default

“Give a component level = "Beginner", then pass nothing, undefined, "", null, and "Advanced". Which ones show Beginner?”

Rename it

“Change ({ title }) to ({ title: courseTitle }) and fix the body. Does the call site change?”

Spread an object

“Spread a course object with an extra id into CourseCard, then remove a required field from it. Which one does TypeScript complain about?”

Wrap a button

“Build Button with ComponentProps<"button">, then pass disabled, className, and onClick. Inspect the DOM.”

What usually goes wrong

Using : for a default

Inside destructuring braces, : renames. A string after it is a syntax error.

✗ ({ level: "Beginner" }: Props)
✓ ({ level = "Beginner" }: Props)
Expecting a default for null or ""

Defaults apply only to undefined. Use ?? if null should fall back too.

✗ ({ level = "Beginner" })  // with level={null} from the API
✓ const shown = level ?? "Beginner";
Destructuring a prop that does not exist

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)
Spreading everything onto the DOM

Custom props in the rest object become unknown DOM attributes, and React warns.

✗ ({ title, ...rest }) => <div {...rest}>
✓ ({ title, lessons, ...divProps }) => <div {...divProps}>
Everything optional

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 };
Using defaultProps on a function component

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)
Destructuring four levels deep

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.

1.

Build a UserCard with name (required), role (default "Student"), and active (default true), showing "Role: Student" and "Status: Active" or "Inactive".

Show hint

Two optional props with defaults.

Show 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: Active
2.

Build 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 hint

A literal union type for level.

Show 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 />;
3.

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 hint

Which of those values is undefined?

Show 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 applies
Coding challenge

A course card with a real API

Design and build a CourseCard whose props type makes the right things required, optional, and defaulted.

It should
  • 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 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

Is props destructuring a React feature?
No. It is JavaScript object destructuring applied to the props object.
When does level = "Beginner" apply?
Only when level is undefined - omitted or passed as undefined. Not for null or "".
What is the difference between { level = "Beginner" } and { level: courseLevel }?
= sets a default; : renames the variable. They combine: { level: courseLevel = "Beginner" }.
What does <CourseCard {...course} /> do?
Passes every property of course as a separate prop - including any extra ones.

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. 1.

    What does function Card({ title }: Props) do? A. Creates state B. Destructures props C. Creates context D. Creates a hook

  2. 2.

    Which gives level a default? A. { level = "Beginner" } B. { level: "Beginner" } C. { level?: "Beginner" }

  3. 3.

    What does { level: courseLevel } do? A. Sets a default B. Renames the prop C. Makes it optional D. Spreads it

  4. 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. 5.

    function Card({ count = 10 }: Props). What is count when the prop is omitted? A. null B. 0 C. 10 D. An error

  6. 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...