Chapters

Hide chapters

React Apprentice

First Edition · web · React 8.0.0 · Visual Studio Code

Section I: Rendering Right

Section 1: 7 chapters
Show chapters Hide chapters

4. Props, Children, and Composition
Written by Eli Ganim

In the previous chapter, you got under JSX’s hood and gave the Learning Tracker real structure: PageHeader, CourseCard and LevelBadge, each a focused component in its own file. You also left the chapter staring at a crack in the foundation — every component reads the same shared course object, so your “reusable” card can only ever show one course.

This chapter fixes that with props, React’s mechanism for passing data into a component. Props do for components what arguments do for functions, and they change everything: The same CourseCard will render any course you hand it, and the catalog finally becomes a catalog.

Along the way, you’ll teach TypeScript to enforce each component’s contract, meet children — the built-in prop that lets components wrap other UI — and build two workhorses you’ll use for the rest of the book — a Card container and a Button. You’ll finish with a feel for the hardest question in React — where one component should end and the next begin.

Feeling the Pain of Hardcoding

Before fixing the problem, make it undeniable. In src/App.tsx, render the card three times:

<main>
  <PageHeader />
  <CourseCard />
  <CourseCard />
  <CourseCard />
</main>

Save and check the browser:

Three cards, one course: reuse without inputs is just repetition.
Three cards, one course: reuse without inputs is just repetition.

Three identical cards. Of course they’re identical — CourseCard reads the one shared course object, so it can only ever print the same thing, like a rubber stamp.

How would you show a second course today? You’d have to copy the entire component. Don’t type this — just imagine your src folder filling up with it:

function CssLayoutCard() {
  return (
    <article>
      <h2>CSS Layout</h2>
      ...
    </article>
  )
}

One near-identical component per course, with the markup duplicated in every one. Fix a typo in the card layout, and you fix it twelve times. This is the moment where every React developer needs what’s coming next.

Props

A component is a function — you’ve known that since Chapter 2. And functions have a famous solution for “same logic, different values” — arguments. React’s version of arguments is props (short for properties).

In JSX, you pass data to a component using attributes — the values you write inside the opening tag, just like HTML attributes. When you write <CourseCard course={reactBasics} />, that course={reactBasics} part is an attribute. React collects all the attributes you give a component and bundles them into a single JavaScript object called the component’s props. That props object is then passed to your component function.

Here’s what happens behind the scenes: When you render <CourseCard course={reactBasics} />, React creates an object like this:

{
  course: reactBasics
}

Then it calls your CourseCard function and hands it that object as its first argument. The attribute name (course) becomes a property name in the props object, and the attribute value (reactBasics) becomes that property’s value. This is how a component receives its inputs.

Every JSX attribute you put on a component becomes a prop — except a couple of special names React reserves for itself, like key, which you’ll meet in Chapter 5:

Props travel in one direction: from parent to child, down the component tree.
Props travel in one direction: from parent to child, down the component tree.

Two rules give props their shape:

  • Props flow one way: A parent passes props down to its children. A child never reaches up to change what it was given — for a component, props are read-only input. This one-way flow is what keeps React apps predictable: To understand what a component shows, you only ever look at what comes in from above.
  • Each component declares what it needs: A card needs a course. A badge needs a level. TypeScript will hold every caller to exactly that — and you’re about to see how.

Passing Your First Prop

Start small with one prop — the course title. In src/CourseCard.tsx, add this type above the component and change the function’s first line, so the file starts like this:

import LevelBadge from './LevelBadge.tsx'
import { course } from './course.ts'

type CourseCardProps = {
  title: string
}

function CourseCard(props: CourseCardProps) {

CourseCardProps is a type alias — a named description of an object’s shape, just like the shapes TypeScript has been inferring for you, except this time you’re the one writing the contract: A card’s props must contain a string called title. Annotating the props parameter with it seals the deal. That’s the whole ceremony, by the way — a component with typed props is still a plain function. No special React type required.

Next, make the card use its new input. Replace the h2 line with:

<h2>{props.title}</h2>

Save, and check your editor: App.tsx now has three errors, one per card:

Property 'title' is missing in type '{}' but required in
type 'CourseCardProps'.

Beautiful, isn’t it? You changed the component’s contract, and TypeScript instantly flagged every call site that no longer honors it. In a real project, that’s a whole class of bugs — the forgotten caller — caught before the page even reloads.

Honor the contract. In src/App.tsx, give each card a title:

<CourseCard title="React Basics" />
<CourseCard title="CSS Layout" />
<CourseCard title="Modern TypeScript" />

Save and check the browser:

One component, three titles — your first prop at work.
One component, three titles — your first prop at work.

Same component, three different headings. The descriptions and levels still match the old shared object — you’ll deal with them shortly — but the principle is proven: Data flows in from the caller, and the component renders whatever it receives.

Note: Writing props.title gets old fast, so React developers reach for destructuring — JavaScript syntax that unpacks an object’s properties into variables in one move. Instead of function CourseCard(props: CourseCardProps) and props.title, you write function CourseCard({ title }: CourseCardProps) and use title directly. The braces in the parameter list mean “pull these properties out of the object I’m given.” It’s the same object arriving either way — destructuring just saves you the props. prefix every time.

Adopt the shorthand now. Change the function’s first line to:

function CourseCard({ title }: CourseCardProps) {

And the h2 back to {title}. Save and check the browser — expect no visible change, because it’s the same object arriving through a shorter door. The book uses this style from here on.

Passing Numbers and Booleans

The title rode into the component in quotes, because it’s a string. Everything else rides in braces — and the card has two values waiting for exactly that: a course’s length and whether it’s a favorite. Extend the contract in src/CourseCard.tsx:

type CourseCardProps = {
  title: string
  durationHours: number
  isFavorite: boolean
}

Destructure all three, wrapping the parameter list for readability:

function CourseCard({
  title,
  durationHours,
  isFavorite,
}: CourseCardProps) {

Inside the component, put both new props to work. Change the favoriteMessage condition from course.isFavorite to plain isFavorite, and add a duration line below the badge:

<p>{durationHours} hours</p>

Then honor the bigger contract in src/App.tsx:

<CourseCard
  title="React Basics"
  durationHours={6}
  isFavorite={true}
/>
<CourseCard
  title="CSS Layout"
  durationHours={4}
  isFavorite={false}
/>
<CourseCard
  title="Modern TypeScript"
  durationHours={9}
  isFavorite={false}
/>

Note what’s in braces and what isn’t: {6} is a number and {false} is a boolean, so they ride in braces like any JSX expression. Try durationHours="6" — quotes instead of braces — and TypeScript pushes back with Type 'string' is not assignable to type 'number'. The attribute syntax looks like HTML, but the values are typed JavaScript.

Save and check the browser:

Strings in quotes, numbers and booleans in braces — each card now owns its title, duration and favorite state.
Strings in quotes, numbers and booleans in braces — each card now owns its title, duration and favorite state.

Three cards, each with its own title, duration and favorite message. Only the shared description and level remain.

Note: For a true boolean, JSX offers a shorthand: Writing the bare attribute name — <CourseCard isFavorite /> — means isFavorite={true}. You’ll see it constantly in other people’s code. This book writes the value out.

Passing the Whole Course

You could keep going one prop at a time — description and level are still waiting — until every call looks like this. Don’t type it:

<CourseCard
  title="React Basics"
  description="Build interfaces from reusable components."
  level="Beginner"
  durationHours={6}
  isFavorite={true}
/>

It works, but those five values aren’t five separate ideas — they’re one course. When props travel together everywhere, pass them as one object.

First, give the app real data to pass. Delete src/course.ts, and create src/courses.ts in its place:

export const reactBasics = {
  title: 'React Basics',
  description: 'Build interfaces from reusable components.',
  level: 'Beginner',
  durationHours: 6,
  isFavorite: true,
}

export const cssLayout = {
  title: 'CSS Layout',
  description: 'Arrange pages with flexbox and grid.',
  level: 'Intermediate',
  durationHours: 4,
  isFavorite: false,
}

export const modernTypescript = {
  title: 'Modern TypeScript',
  description: 'Master types for safer, clearer code.',
  level: 'Advanced',
  durationHours: 9,
  isFavorite: false,
}

Three courses, three named exports from one file — which is exactly what named exports are for. Notice the new durationHours property — a number, not a string, because you’ll want to do number things with it later.

Now rebuild the card around a single object prop. Replace the entire contents of src/CourseCard.tsx with:

import LevelBadge from './LevelBadge.tsx'

type CourseCardProps = {
  course: {
    title: string
    description: string
    level: string
    durationHours: number
    isFavorite: boolean
  }
}

function CourseCard({ course }: CourseCardProps) {
  let favoriteMessage = 'Not in your favorites yet'
  if (course.isFavorite) {
    favoriteMessage = 'One of your favorites'
  }

  return (
    <article>
      <h2>{course.title}</h2>
      <p>{course.description}</p>
      <LevelBadge level={course.level} />
      <p>{course.durationHours} hours</p>
      <p>{favoriteMessage}</p>
    </article>
  )
}

export default CourseCard

Three things changed. The props type now declares one prop, course, whose value is an object with the full course shape — a type within a type. The component reads everything from that object, including the new duration line. And the import of the shared course data is gone: The card no longer knows where courses come from, it just renders whichever one arrives. That nested shape in the props type is begging for a name of its own, by the way — it gets one in Chapter 5.

The card also passes level={course.level} along to LevelBadge — a prop handed down through two levels of the tree. Make the badge ready to receive it. Replace the contents of src/LevelBadge.tsx with:

type LevelBadgeProps = {
  level: string
}

function LevelBadge({ level }: LevelBadgeProps) {
  return <p>Level: {level}</p>
}

export default LevelBadge

Like the card, the badge dropped its data import. It went from “the component that shows the course’s level” to “the component that shows a level” — a small wording change and a big reusability change.

Finally, connect the data. Replace the contents of src/App.tsx with:

import './App.css'
import PageHeader from './PageHeader.tsx'
import CourseCard from './CourseCard.tsx'
import {
  reactBasics,
  cssLayout,
  modernTypescript,
} from './courses.ts'

function App() {
  return (
    <main>
      <PageHeader />
      <CourseCard course={reactBasics} />
      <CourseCard course={cssLayout} />
      <CourseCard course={modernTypescript} />
    </main>
  )
}

export default App

Note the braces in course={reactBasics}: you’re passing a JavaScript object, not a string, so it rides in the same braces as every other expression in JSX. Quotes are only for literal strings like title="React Basics" earlier.

Save and check the browser:

Three genuinely different cards from one component and three objects.
Three genuinely different cards from one component and three objects.

There’s your catalog — three courses with their own titles, descriptions, levels, durations and favorite states — and exactly one card component behind all of them.

Handling Optional Props

Real data is never uniform. Say the CSS Layout course is self-paced — it has no fixed length. In src/courses.ts, delete the durationHours: 4, line from cssLayout and save. TypeScript objects in App.tsx:

Property 'durationHours' is missing in type '{ title: string;
description: string; level: string; isFavorite: boolean; }' but
required in type ...

The contract says every course has a duration, and cssLayout no longer does. You could invent a fake duration, but the honest fix is to loosen the contract: Durations are optional. In CourseCardProps, change the duration line to:

durationHours?: number

The ? before the colon marks the property as optional: Callers may omit it, and inside the component its value is either a number or undefined. The error disappears — but check the browser: The CSS Layout card now shows a lonely “ hours”. That’s the undefined rendering as nothing (as you learned in Chapter 3) while the fixed text soldiers on.

An optional input deserves a deliberate fallback. In CourseCard, replace the duration line inside the JSX with <p>{durationLabel}</p>, and compute the label above the return, below favoriteMessage:

let durationLabel = 'Self-paced'
if (course.durationHours !== undefined) {
  durationLabel = course.durationHours + ' hours'
}

The Chapter 2 pattern again: Decide before the return, render the result. Save and check the browser:

Optional data, handled honestly: no duration means self-paced, not a blank.
Optional data, handled honestly: no duration means self-paced, not a blank.

Children: The Prop You Don’t Pass by Name

Every prop so far carried data. One special prop carries UI. Compare these two lines of JSX you’ve written in this book:

<CourseCard course={reactBasics} />
<main>...</main>

The main element wraps content between its tags. Your components can do that too — and whatever you put between a component’s opening and closing tags arrives as a prop named children. That’s the key to building container components — pieces that provide a consistent shell and let the caller decide what goes inside.

Your card is about to become one. Create src/Card.tsx:

import type { ReactNode } from 'react'

type CardProps = {
  children: ReactNode
}

function Card({ children }: CardProps) {
  return <article>{children}</article>
}

export default Card

Two newcomers here. ReactNode is React’s type for “anything React can render” — elements, strings, numbers, fragments, or a mix — and it’s the right type for children almost every time. And import type tells TypeScript this import is a type, not a value; it vanishes at compile time. The component itself is almost comically simple: Take whatever the caller wrapped and render it inside an article. In Chapter 6, this one component becomes the place where every card’s styling lives — write the shell once, and every card in the app gets the upgrade.

Put it to work through composition — building a component out of other components. In src/CourseCard.tsx, import the new container at the top of the file, above the LevelBadge import:

import Card from './Card.tsx'

Then change the returned JSX’s outer element from <article>/</article> to <Card>/</Card>. The card’s content — heading, paragraphs, badge — is now Card‘s children. Save and check the browser: No visible change, because Card renders the same article the code rendered directly before. The structure changed; the DOM didn’t.

Building a Reusable Button

The catalog needs actions — starting with a way to mark favorites. Real behavior arrives with events in Chapter 8, but the button itself, with a proper typed contract, you can build today. Create src/Button.tsx:

type ButtonProps = {
  label: string
  onClick: () => void
  variant?: 'primary' | 'ghost'
}

function Button({
  label,
  onClick,
  variant = 'primary',
}: ButtonProps) {
  return (
    <button
      type="button"
      className={'button ' + variant}
      onClick={onClick}
    >
      {label}
    </button>
  )
}

export default Button

This little component carries three new ideas, one per prop:

  • A function prop: onClick’s type, () => void, reads as “a function that takes no arguments and returns nothing.” Functions are values in JavaScript, so they pass through props like any other value — this is how a parent tells a reusable component what to do, not just what to show.
  • A union of exact strings: 'primary' | 'ghost' means this prop accepts those two strings and nothing else. Not any string — those two. You’ll give unions a proper look in Chapter 5.
  • A default value: variant = 'primary' in the destructuring supplies the value when the caller omits the optional prop. Optional on the outside, always defined on the inside.

The component pins type="button" (so the button never accidentally submits a form), builds a class name from the variant — inert until the styling work in Chapter 6 — and hands your onClick to the real DOM button.

Now give every card the button. In src/CourseCard.tsx, import it below the LevelBadge import:

import Button from './Button.tsx'

Add a click function inside CourseCard, below the durationLabel block:

function noteFavoriteClick() {
  console.log('Favorites arrive in Chapter 8!')
}

And add the button at the bottom of the card, below the favorite message:

<Button
  label="Add to favorites"
  onClick={noteFavoriteClick}
/>

Look closely at onClick={noteFavoriteClick} — no parentheses. You’re passing the function itself as a value, not calling it. If you wrote noteFavoriteClick(), you’d pass its result instead — a classic mistake worth meeting now, on purpose, so it can’t ambush you in Chapter 8.

Save and check the browser:

Every card grows a button — typed, labeled and waiting for Chapter 8.
Every card grows a button — typed, labeled and waiting for Chapter 8.

Each card has a button. Click one with the browser console open (right-click the page, Inspect, then the Console tab), and “Favorites arrive in Chapter 8!” appears — proof the function traveled from CourseCard through the onClick prop to the real button. That console message is the full extent of today’s interactivity; making clicks change what’s on screen is Chapter 8’s whole story.

One more thing while you’re here: Try misspelling the variant. Add variant="primry" to the button’s props and watch TypeScript respond:

Type '"primry"' is not assignable to type
'"primary" | "ghost" | undefined'. Did you mean '"primary"'?

A plain string type would have waved that typo straight through to production. The union caught it and suggested the fix. Delete the variant line — the default covers it — then save and check the browser one last time: The buttons look exactly as they did before the experiment.

Drawing Component Boundaries

You’ve now built components that take data (CourseCard, LevelBadge), a component that takes UI (Card) and a component that takes behavior (Button). The remaining skill is judgment: How much should one component do?

Prop overload versus composition: the same card, designed two ways.
Prop overload versus composition: the same card, designed two ways.

The left card in the picture is where components go to die: Every new requirement became another prop, until the component needs twelve inputs, and nobody remembers which combinations work. That’s prop overload, and it’s the main symptom of a component trying to do too many jobs. You’ll hear these called mega-components, and they grow in every codebase that never stops to refactor.

The right side is what you actually built today. Warning signs that a component wants splitting, for your future projects:

  • The props list keeps growing, and some props only matter when other props have certain values.
  • You can’t describe the component’s job in one short sentence.
  • Two parts of the component change for unrelated reasons — the card layout changed, so you scrolled past the button logic to find it.

When you spot these, do what you did in this chapter: Pull out the piece with a name of its own — a badge, a button, a shell — give it a small, typed contract and compose. Small components with clear props are cheap to build and cheaper to change. That’s not a style preference; it’s the compounding interest that keeps month-twelve you moving as fast as day-one you.

Challenge: Build a Section Heading

The catalog’s cards now sit directly under the page header. Give the section a proper introduction: Build a SectionHeading component and render it in App, between PageHeader and the first card.

The contract — a required title string and an optional subtitle string. It renders the title as an h2. When a subtitle is provided, it renders it in a p below; when it’s omitted, no empty p sneaks into the page. Call it like this:

<SectionHeading
  title="Course Catalog"
  subtitle="Three courses and counting."
/>

A few hints:

  • Two sibling elements from one component — you know the tool for that.
  • For the optional subtitle, remember that JSX stored in a variable is just a value, and that rendering null produces nothing. Decide above the return, like the duration label.
  • Type the props with a ? where the contract says optional.

You’ll find a complete solution in the challenge folder of this chapter’s materials.

Key Points

  • Props are a component’s inputs: JSX attributes collected into one object and passed to your function, like arguments.
  • Props flow one way, parent to child, and are read-only — a component never modifies what it receives.
  • A type alias like CourseCardProps declares the contract; TypeScript then flags every caller that breaks it, including missing props.
  • Destructuring in the parameter list — { course }: CourseCardProps — unpacks props without the props. prefix.
  • Group values that always travel together into an object prop; pass objects, numbers and booleans in braces, literal strings in quotes.
  • A ? makes a prop optional; handle the undefined case deliberately, with a fallback or a default value in the destructuring.
  • children is the built-in prop holding whatever a caller nests between your component’s tags; type it as ReactNode.
  • Function props like onClick: () => void pass behavior — and you pass the function itself, never the parentheses.
  • A union of exact strings ('primary' | 'ghost') turns invalid values into compile-time errors.
  • Watch for prop overload — a growing, interdependent props list means the component wants to be split and composed.

Where to Go From Here?

The catalog is honest now — three real courses, one reusable card. But look at App.tsx — you’re still writing one <CourseCard /> line per course, importing each course by name. Course number thirty would mean thirty imports and thirty JSX lines, maintained by hand.

Courses belong in a list — a typed array the UI renders automatically, however many there are. That’s the next chapter: arrays, map, the mysterious key prop, and making the page respond gracefully to whatever the data holds — including nothing at all.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.