Props and Children
Props (short for “properties”) are how data flows into a React component from its parent, similar to how you’d pass arguments into a function. children is a special prop that holds whatever content is nested between a component’s opening and closing tags. Together, props and children are the foundation of component composition — they let you build small, reusable pieces of UI and combine them into larger interfaces without ever mutating the DOM directly.
Overview / How it works
Every React component is, at its core, just a JavaScript function that accepts a single argument: an object of props. When you write <Greeting name="Ava" />, React calls Greeting({ name: "Ava" }) under the hood and uses whatever JSX that function returns to build the UI. Any attribute you write on a JSX element — strings, numbers, booleans, arrays, objects, even functions — becomes a key on that props object.
Props are read-only. A component must never reassign or mutate the props object it receives; it can only read from it and use those values to decide what to render. This is a deliberate constraint: React’s rendering model assumes that a component’s output is a pure function of its props and state. If a component silently changed its own props, that assumption would break, and React would have no way to know when the UI actually needs to be recomputed. Data should flow in one direction — parent to child — which is why this pattern is often called “unidirectional data flow.” If a child needs to change something, the parent passes it a callback function as a prop, and the child calls that function; it never edits the prop directly.
children is simply the prop React automatically populates with whatever you put between a component’s tags. <Card><p>Hello</p></Card> results in Card receiving props.children equal to the <p>Hello</p> element. This is what lets you build wrapper or “layout” components — a Card, a Modal, a Layout — that don’t know or care what content they’ll eventually contain. The parent decides the content; the child component decides how to frame it.
When a parent re-renders, React calls each child component function again with a fresh props object and compares (“reconciles”) the newly returned JSX tree against the previous one. If a prop’s value is different from the last render (checked with a shallow Object.is comparison per key), React updates only the specific DOM nodes that actually changed during the commit phase — it does not tear down and rebuild the whole subtree. If none of a component’s props (or state) changed, by default it still re-renders when its parent re-renders, though the resulting DOM patch will be a no-op; tools like memo exist to skip that re-render entirely when props are unchanged, but that’s a topic for a later lesson.
| Concept | Owned by | Mutable? | Purpose |
|---|---|---|---|
| Props | Parent (passed down) | No, read-only | Configure a component from the outside |
| children | Parent (nested JSX) | No, read-only | Let a parent supply nested content/markup |
| State | The component itself | Yes, via its setter | Track data that changes over time |
Syntax
function ComponentName(props) {
return <div>{props.someValue}</div>;
}
// Usage
<ComponentName someValue="hello" />
<ComponentName someValue="hello">
<span>nested content</span>
</ComponentName>
- props — the single object argument every function component receives; contains every attribute passed on the JSX tag.
- props.someValue — reading an individual prop; you can also destructure it directly in the parameter list:
function ComponentName({ someValue }). - props.children — automatically set to whatever JSX (or text, or an array of elements) is nested between the component’s opening and closing tags. It is
undefinedif the tag is self-closing. - Default values — set with destructuring defaults, e.g.
function ComponentName({ someValue = "default" }), used only when the prop isundefined.
Examples
Example 1: Basic props
function Greeting(props) {
return (
<p>
Hello, {props.name}! You are {props.age} years old.
</p>
);
}
function App() {
return (
<div>
<Greeting name="Ava" age={7} />
<Greeting name="Leo" age={9} />
</div>
);
}
export default App;
Renders: two paragraphs — “Hello, Ava! You are 7 years old.” and “Hello, Leo! You are 9 years old.” Each <Greeting /> call passes different values for the name and age props, so the same component produces different output each time. Note that age={7} uses curly braces because the value is a JavaScript number, not a string.
Example 2: Destructuring props with a default value
function Button({ label, onClick, variant = "primary" }) {
return (
<button className={`btn btn-${variant}`} onClick={onClick}>
{label}
</button>
);
}
function App() {
const handleClick = () => alert("Clicked!");
return (
<div>
<Button label="Save" onClick={handleClick} />
<Button label="Delete" onClick={handleClick} variant="danger" />
</div>
);
}
export default App;
Renders: two buttons, “Save” (class btn btn-primary) and “Delete” (class btn btn-danger). Destructuring props directly in the function signature — { label, onClick, variant = "primary" } — is the idiomatic way to read them in modern React; it avoids repeating props. everywhere and makes it obvious at a glance exactly what data the component expects. Because variant has a default, the “Save” button falls back to "primary" since no variant prop was passed for it.
Example 3: Composing with children
function Card({ title, children }) {
return (
<div className="card">
<h3>{title}</h3>
<div className="card-body">{children}</div>
</div>
);
}
function App() {
return (
<Card title="Weather">
<p>Sunny, 72°F</p>
<button>Refresh</button>
</Card>
);
}
export default App;
Renders: a div.card containing an h3 reading “Weather” and a div.card-body that wraps a paragraph (“Sunny, 72°F”) and a “Refresh” button. Card never needs to know what’s inside it — the <p> and <button> are supplied entirely by App as children. This is the pattern behind most layout, modal, and panel components: the wrapper owns the chrome (borders, headings, spacing), and the caller owns the content.
Example 4: Functions as props (render props) with a list
function List({ items, renderItem }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{renderItem(item)}</li>
))}
</ul>
);
}
function App() {
const fruits = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Cherry" },
];
return (
<List
items={fruits}
renderItem={(fruit) => <strong>{fruit.name}</strong>}
/>
);
}
export default App;
Renders: an unordered list with three items, each showing a fruit name in bold (“Apple“, “Banana“, “Cherry“). Props aren’t limited to strings and numbers — here renderItem is a function passed as a prop, letting List stay completely generic about how each item is displayed while App controls the actual markup. Notice the key={item.id} on each <li>; React needs a stable, unique key on every element produced from an array so it can track which items moved, changed, or were removed between renders.
Under the hood: props and the render cycle
On mount: React calls the component function with its initial props object, gets back a JSX tree (really a tree of React elements), converts that into real DOM nodes, and inserts them into the page.
On a prop update: when a parent re-renders and passes a new props object to a child (even if only one field changed), React calls the child function again with the new props. It builds the new element tree, diffs it against the previous tree during reconciliation, and only touches the specific DOM nodes/attributes that actually differ during the commit phase — it does not remove and recreate the entire child element. This is why updating a single prop, like changing a button’s label, only updates that button’s text node rather than re-creating the whole button.
On unmount: if the parent stops rendering a child (for example, it was inside a conditional that’s now false, or removed from a list), React removes that component’s DOM nodes and discards its internal state; any cleanup registered in that component’s effects runs at this point.
Common Mistakes
Mistake 1: Mutating props directly
function Profile(props) {
props.name = props.name.toUpperCase(); // ❌ mutating the props object
return <p>{props.name}</p>;
}
This works by accident in plain JavaScript, but it violates React’s core rule that props are read-only. It can cause confusing bugs because the parent’s original data may be shared or reused, and mutating it directly can affect other parts of the app that also hold a reference to that same object. Instead, derive a new value and use that:
function Profile({ name }) {
const displayName = name.toUpperCase();
return <p>{displayName}</p>;
}
Mistake 2: Forgetting to render children
function Wrapper({ title }) {
return (
<div className="wrapper">
<h2>{title}</h2>
</div>
);
}
// <Wrapper title="Info"><p>This text vanishes</p></Wrapper>
Here children was never destructured or used, so anything nested inside <Wrapper> is silently dropped — no error is thrown, the content just never appears. Whenever a component is meant to wrap arbitrary content, make sure it accepts and renders children:
function Wrapper({ title, children }) {
return (
<div className="wrapper">
<h2>{title}</h2>
{children}
</div>
);
}
Best Practices
- Treat props (including
children) as read-only — never reassign or mutate them; derive new values into local variables instead. - Destructure props in the function parameter list (
function Card({ title, children })) instead of repeatingprops.throughout the component — it documents the component’s expected API at a glance. - Give props sensible default values with destructuring defaults (
variant = "primary") rather than checking forundefinedmanually inside the function body. - Keep prop names descriptive and consistent across similar components (prefer
onClick,onSubmit,onChangefor callbacks to match native DOM event naming conventions). - Use
childrenfor “slot”-style composition (cards, modals, layouts) instead of a prop likecontentorbodyTextwhen the content is JSX rather than plain text. - Always add a stable, unique
keyprop when rendering elements from an array — never use the array index as the key if the list can be reordered, filtered, or have items inserted. - Pass functions as props for child-to-parent communication instead of trying to have a child directly modify a prop it received.
Practice Exercises
- Exercise 1: Create a
UserCard({ name, role })component that renders the person’s name and role. Render it three times in anAppcomponent with different names and roles. - Exercise 2: Build a
Panel({ heading, children })component that renders a heading followed by whatever children are passed to it. Use it to wrap a short paragraph and a list inside two separate panels. - Exercise 3: Write a
Badge({ text, color = "gray" })component that renders<span>with an inline class likebadge badge-{color}. Render a list of badges from an array of objects (e.g.{ id, text, color }), remembering to add a uniquekeyto each one.
Summary
- Props are read-only data passed from a parent component to a child, similar to function arguments.
- A function component receives a single props object; destructuring it in the parameter list is the idiomatic way to read individual props.
childrenis a special prop automatically populated with whatever JSX is nested between a component’s opening and closing tags.- Never mutate props or children — always derive new values instead, keeping data flow strictly one-directional (parent to child).
- When a prop changes, React re-renders the component function and reconciles the new output against the previous render, updating only the DOM that actually changed.
- Functions can be passed as props too, enabling both child-to-parent communication (callbacks) and flexible rendering patterns (render props).
- Always add a unique
keywhen rendering a list of elements generated from an array.
