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 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:
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:
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.titlegets old fast, so React developers reach for destructuring — JavaScript syntax that unpacks an object’s properties into variables in one move. Instead offunction CourseCard(props: CourseCardProps)andprops.title, you writefunction CourseCard({ title }: CourseCardProps)and usetitledirectly. 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 theprops.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:
Three cards, each with its own title, duration and favorite message. Only the shared description and level remain.
Note: For a
trueboolean, JSX offers a shorthand: Writing the bare attribute name —<CourseCard isFavorite />— meansisFavorite={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:
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:
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:
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?
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
nullproduces nothing. Decide above thereturn, 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
CourseCardPropsdeclares the contract; TypeScript then flags every caller that breaks it, including missing props. -
Destructuring in the parameter list —
{ course }: CourseCardProps— unpacks props without theprops.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 theundefinedcase 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: () => voidpass 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.