Props & composition.
How components talk to each other.
A React app is a tree of components. Props are how data flows through that tree — from parent to child, always one direction. Composition is how small components combine into bigger ones. Together, they're the mechanism that makes React apps modular and predictable. Once you understand props, you understand 80% of how React apps are structured.
Props are how data flows from parent to child in React. Pass them with JSX attributes: <Greeting name="Alice" age={30} />. The child receives them as an object: function Greeting(props) { ... }. Props are read-only from the child's perspective — a child can display them but cannot change them. When the parent passes new props, the child re-renders. This one-way data flow is what makes React apps predictable.
Before this lesson: complete Lesson 1. You need to be comfortable with JSX and function components.
📤 Passing props — parent to child
JSX attributes. String values in quotes. Everything else in curly braces.
Props are passed using JSX attributes, the same way you pass attributes to HTML elements. String values use quotes (name="Alice"). Numbers, variables, functions, objects, and arrays use curly braces (age={30}, onClick={handleClick}). Everything inside the angle brackets becomes one props object that the child receives.
// The child component receives props as its first argument function Greeting(props) { return ( <div> <h1>Hello, {props.name}!</h1> <p>You are {props.age} years old.</p> </div> ); } // The parent passes props as JSX attributes function App() { return ( <div> <Greeting name="Alice" age={28} /> <Greeting name="Bob" age={34} /> <Greeting name="Priya" age={31} /> </div> ); } export default App;
String values
Use quotes: name="Alice". The value is passed as a string. This is what JSX treats as literal text.
Everything else
Use curly braces: age={28}, items={[1, 2, 3]}, onClick={handleClick}. The value is a JavaScript expression evaluated at render.
Props are read-only
The child cannot modify props.name. Attempting it fails or is ignored. Data flows one way: down. This is by design.
Reusable components
One Greeting component renders three different outputs because of props. This is the entire point — write once, use many times with different data.
Props are function arguments. Think of a component as a function and props as its arguments. <Greeting name="Alice" /> is exactly like calling Greeting({ name: "Alice" }). Same mechanics. React just uses a special syntax for it, and enforces that the function (component) is always called the same way (via JSX). Once you see the function equivalent, props feel less magical.
📦 Destructuring props — the modern pattern
Extract props in the parameter list. Cleaner than props.name everywhere.
// BEFORE — verbose function Greeting(props) { return ( <div> <h1>Hello, {props.name}!</h1> <p>You are {props.age} years old.</p> <p>Your email is {props.email}.</p> </div> ); } // AFTER — destructured in the parameter list function Greeting({ name, age, email }) { return ( <div> <h1>Hello, {name}!</h1> <p>You are {age} years old.</p> <p>Your email is {email}.</p> </div> ); } // Both versions work identically. // Destructuring is just cleaner — no props. prefix everywhere. // Same props passed: <Greeting name="Alice" age={28} email="a@b.com" />
Object destructuring
{ name, age, email } in the parameter list extracts those properties from the props object. It's the same destructuring you learned in JavaScript — applied to function arguments.
Rename with :
{ name: userName } destructures but renames to userName. Useful when a prop name conflicts with something in scope.
Nested destructuring
{ user: { name, age } } extracts from a nested object. If user might be undefined, use defaults or optional chaining.
Rest for the remaining
{ name, ...rest } — puts remaining props in rest. Useful for forwarding unknown props: <div {...rest} />.
📥 The children prop — wrap anything
Whatever is between the tags becomes props.children. The key to layout components.
// A Card component that wraps whatever is between its tags function Card({ children }) { return ( <div style={{ border: '1px solid #334155', borderRadius: '8px', padding: '1rem', background: '#1e293b', }}> {children} </div> ); } // Use it — the children can be anything function App() { return ( <div> <Card> <h2>User Profile</h2> <p>Alice Chen</p> <p>alice@example.com</p> </Card> <Card> <img src="photo.jpg" alt="Profile" /> <button>Edit</button> </Card> <Card> Just some text </Card> </div> ); }
Anything can be children
Text, elements, other components, arrays, fragments. Whatever is between <Card> and </Card> becomes props.children.
Layout components
Card, Modal, Layout, Panel, Container — every "wrapper" component uses children. That's how they wrap arbitrary content without knowing what it is.
Composition over props
Instead of <Card title="X" subtitle="Y" body="Z" />, use <Card><h2>X</h2><p>Y</p></Card>. The reader sees structure; the Card doesn't need to know.
Multiple children
children is a value — it can be a single element, an array of elements, or text. React handles all cases. Use React.Children.map if you need to iterate.
Children is React's most powerful composition primitive. Instead of building a Card with 15 props for every possible content variation, build one Card that accepts children. The parent decides what goes inside. This pattern — "wrap, don't configure" — is what makes React components composable. It's the difference between a component that handles 5 cases and one that handles infinite cases.
🎛️ Default values — when a prop is missing
Destructure with = to give props a fallback. Cleaner than ternaries everywhere.
// Default values via destructuring function Greeting({ name = 'World', age = 0, role = 'user', }) { return ( <div> <h1>Hello, {name}!</h1> <p>Age: {age}, Role: {role}</p> </div> ); } // All of these work: <Greeting /> // "Hello, World!" age 0, role user <Greeting name="Alice" /> // "Hello, Alice!" age 0, role user <Greeting name="Bob" age={30} /> // "Hello, Bob!" age 30 <Greeting name="Carol" age={25} role="admin" /> // everything overridden // If a prop is required — no default — passing it is the parent's job. // If the parent forgets, the child gets undefined and renders "Hello, !" // which is a bug. TypeScript or PropType checks catch this.
Only fires on undefined
Default applies when the prop is undefined (not passed). Passing null, 0, or "" does NOT trigger the default — those are real values.
Required vs optional
If a prop has no default, it's effectively required. If the parent forgets, the child gets undefined. TypeScript or PropTypes catches this; plain JavaScript doesn't.
Computed defaults
The default can be any expression: createdAt = new Date(). It's evaluated on every render, not cached. For expensive values, compute inside the component.
defaultProps (legacy)
Greeting.defaultProps = { name: 'World' } — the old way. Deprecated in React 19 in favor of destructuring defaults. Don't use it in new code.
🔔 Callback props — child talks to parent
Pass a function as a prop. The child calls it. This is how events flow up.
Props flow down (parent → child). But what if a child needs to trigger something in the parent — like deleting an item or submitting a form? The parent passes a function as a prop; the child calls it when something happens. That's the entire mechanism for upward data flow.
// Parent passes a function as a prop function App() { function handleDelete(name) { alert(`Deleting ${name}`); } return ( <div> <UserCard name="Alice" onDelete={handleDelete} /> <UserCard name="Bob" onDelete={handleDelete} /> </div> ); } // Child receives the function and calls it function UserCard({ name, onDelete }) { return ( <div> <span>{name}</span> <button onClick={() => onDelete(name)}> Delete </button> </div> ); } // Data flow: // 1. Parent owns the delete logic (handleDelete) // 2. Parent passes it to child (onDelete={handleDelete}) // 3. Child calls it on button click (onDelete(name)) // 4. Parent's function runs, does the actual deletion // This is "state down, events up" in action.
onSomething naming
Callback props start with "on": onClick, onChange, onDelete, onSubmit. This tells the reader "this prop is a function to call when X happens."
Pass the function, don't call it
onClick={handleClick} — passes the function. onClick={handleClick()} — calls it immediately during render, not on click. This is the #1 mistake.
Arrow functions for arguments
If the child needs to pass arguments to the callback: onClick={() => onDelete(name)}. The arrow wraps the call so it happens on click, not on render.
One direction
Data flows down (props). Events flow up (callbacks). Never the other way. This makes every React app predictable — you always know where data comes from and where changes originate.
"Data down, events up" is the entire React data model. Every app you build follows this pattern: state lives in a parent, passed down via props to children. When something happens (click, form submit), the child calls a callback prop, which the parent handles. That's it. Every React app — from a to-do list to Facebook — uses exactly this pattern at every level. Once you internalize it, React's "complexity" disappears.
🕳️ Prop drilling — when to worry
Passing props through components that don't use them. Fine at 2-3 levels, painful beyond.
// Prop drilling — theme travels through 3 components function App() { const theme = 'dark'; return <Layout theme={theme} />; } function Layout({ theme }) { // Layout doesn't use theme — just passes it down return <Sidebar theme={theme} />; } function Sidebar({ theme }) { // Sidebar doesn't use theme — just passes it down return <Button theme={theme} />; } function Button({ theme }) { // Finally — Button actually uses it return <button className={theme}>Click</button>; } // The problem: adding a new prop means touching Layout AND Sidebar, // even though neither of them uses it. // ───────────────────────────────────────── // Fix 1: Context — theme accessible anywhere const ThemeContext = createContext('light'); function App() { return ( <ThemeContext.Provider value="dark"> <Layout /> </ThemeContext.Provider> ); } function Button() { const theme = useContext(ThemeContext); return <button className={theme}>Click</button>; } // Layout and Sidebar don't need to know about theme at all. // Fix 2: Composition — pass children instead of drilling function App() { const theme = 'dark'; return ( <Layout> <Sidebar> <Button theme={theme} /> </Sidebar> </Layout> ); }
2-3 levels is fine
Drilling through 2-3 components is normal and readable. Don't add Context just because a prop travels two levels.
4+ levels — use Context
When a prop travels through 4+ components that don't use it, use Context. The reader sees clearly where data comes from.
Composition first
Before Context, try composition. Pass children instead of drilling — <Layout><Button theme={theme} /></Layout> — Layout doesn't need to know about theme.
Global state (Zustand, Redux)
For app-wide state (user session, cart) used across many components, use a state library. Context works but re-renders everything on change; Zustand is more selective.
🏆 Challenge
Three tasks. Each exercises a different props pattern.
Task 1 — Build a reusable Card
Create a Card component that:
1. Accepts a title prop (string, required)
2. Accepts a variant prop with default "default" (options: "default", "warning", "success")
3. Accepts children for the body content
4. Renders with a border color matching the variant
5. Renders the title in a heading, then the children below
Use it to render three cards in App: one default, one warning, one success. Each should have different content. This teaches props, defaults, and children in one component.
Task 2 — Build a UserList
Create two components:
UserCard — accepts name, email, and onDelete (function). Renders the name, email, and a "Delete" button. Button click calls onDelete(name).
UserList — accepts a users array and onDelete function. Maps over the users, renders a UserCard for each. Pass the same onDelete to every card.
In App, define the users array and the delete handler, pass them to UserList. Test that clicking "Delete" on any card calls the parent's handler with the correct name.
Task 3 — Spot the props bugs
These pieces of code have bugs. What's wrong with each?
1. <Greeting name="Alice" age="28" /> — child does age + 10, gets "2810" instead of 38
2. onClick={handleDelete} vs onClick={() => handleDelete(name)} — why does the second one work?
3. function Greeting(props) { props.name = 'Bob'; return <p>{props.name}</p> } — what happens?
4. Child receives children but renders nothing — what went wrong?
5. Passing a prop that the child doesn't destructure — does the child see it?
Rewrite each fix. If you can't figure out the answer, run the code and read the error message — that's the exercise.
⚠️ Common mistakes
The props bugs that catch every beginner.
onClick={handleClick()} calls handleClick on every render, not on click. Fix: onClick={handleClick} (no parens) or onClick={() => handleClick()}.
age="28" passes the string "28", not the number 28. Fix: age={28}. Same for booleans: disabled="true" is truthy but wrong; use disabled={true} or just disabled.
props.name = 'Bob' has no effect (or throws in strict mode). Fix: if the child needs to change a value, the parent must own it and pass a callback.
function Card({ children }) { return <div></div> } — the children prop is received but never rendered. Fix: <div>{children}</div>.
<button /> is a native HTML button. <Button /> is your component. Capitalization matters — lowercase names are treated as HTML tags.
Passing the same prop through 5 layers is a maintenance burden — every layer must know about it. Fix: Context, composition, or state management library.
{users.map(u => <UserCard {...u} />)} warns about missing keys. Fix: {users.map(u => <UserCard key={u.id} {...u} />)}.
<Child config={{ a: 1 }} /> creates a new object every render, causing Child to re-render every time. Fix: define the object outside the render or use useMemo.
➡️ What's next
You can pass data. Next: make components interactive.
❓ Frequently asked questions
What are props in React?
Props (short for 'properties') are how data flows from a parent component to a child component. They're read-only from the child's perspective — a child can display a prop but cannot modify it. When a parent passes new props, the child re-renders.
How do I pass props to a component?
Use JSX attributes: <Greeting name='Alice' age={30} />. Everything inside the angle brackets is passed as a single props object. The child receives it as the first argument. String values use quotes; everything else uses curly braces.
What is the difference between props and state?
Props are data passed from parent to child — immutable from the child's side. State is data a component owns and can change over time. When props change, the component re-renders. When state changes, the component re-renders.
What is the children prop in React?
The children prop is a special prop that contains whatever you write between a component's opening and closing tags. <Card>Hello</Card> — 'Hello' is available inside Card as props.children.
How do I give a prop a default value?
Use destructuring with a default: function Greeting({ name = 'World' }) { ... }. If name isn't passed, it's 'World'. The default only fires on undefined — not on null, 0, or empty string.
What is prop drilling and how do I avoid it?
Prop drilling is when you pass props through many component layers that don't use them. Fixes: useContext (put the value in context and read it anywhere), component composition (pass children instead of drilling), or a state management library for complex apps.
Can I mutate props inside a child component?
No. Props are read-only from the child's perspective. This is by design: React wants data to flow one way (down) and events to flow the other (up). If a child needs to change data, it calls a callback prop provided by the parent.
What is the difference between props and attributes in HTML?
They're the same idea applied to different things. HTML attributes configure HTML elements. React props configure React components. The syntax is identical; the difference is what they configure and that props can be any JavaScript value.