React 9 ⚛️ Passing and Reading Props
Props are the mechanism by which data flows from a parent component to a child component in React. They are the arguments to a component function, read-only, and immutable within the receiving component. A parent passes props by writing attributes in JSX; a child reads them from the single props object it receives. This one-directional flow — parent to child, never child to parent — is the foundation of React’s predictable data model. When data needs to flow the other way, the parent passes a function as a prop, and the child calls it.
Props are not state. State is data a component owns and can change; props are data a component receives and cannot change. The distinction matters because it determines where data lives and how it changes. A component that receives a prop it needs to modify must either lift the state to a common ancestor or maintain its own derived state. Understanding this boundary prevents the common mistake of trying to mutate props.
This chapter covers passing props, reading props with destructuring, default values, the children prop, prop types with TypeScript, the spread operator, callback props, and the patterns that keep prop flow clean as component trees grow.
Key point: Props flow from parent to child and are read-only in the child. A parent passes data as JSX attributes; the child receives them as a single object and destructures what it needs. To send data upward, the parent passes a callback function as a prop.
Why props exist
The data flow problem. A component needs data to render. Where does that data come from? A component could fetch it, or read it from a global, or receive it from its parent. Props are the answer: the parent provides the data, and the child renders it. This makes the flow explicit and traceable. You can look at a component’s JSX usage and see exactly what data it receives.
The reuse problem. A component that hardcodes its data cannot be reused. A component that receives its data as props can be rendered with different data in different places. <UserCard user={alice} /> and <UserCard user={bob} /> render the same component with different props. Reuse depends on parameterization, and props are the parameterization mechanism.
The immutability problem. Props must not be mutated by the receiving component. If a child could change its props, the parent’s data would change without the parent’s knowledge, and the source of truth would be unclear. React enforces one-way flow: data goes down, events go up. When a child needs to change something, it calls a callback prop, and the parent decides what to do.
The testing problem. A component that receives all its data as props is a pure function: given the same props, it renders the same output. This makes it trivial to test. Render the component with known props and assert on the output. No mocking of network requests or global state is needed for the component itself.
The composition problem. Props compose. A parent can pass any value as a prop: a string, a number, an object, an array, a function, or a React element. This flexibility allows components to be composed in ways the original author may not have anticipated. A Layout component that accepts a header prop can render any header the caller provides.
a. Passing props from parent to child
Props are passed as attributes in JSX. Any attribute becomes a property on the child’s props object.
function Welcome(props) {
return <h1>Hello, {props.name}!</h1>;
}
function App() {
return <Welcome name="Alice" />;
}
The name attribute becomes props.name in the child. Multiple props become multiple properties:
function UserCard(props) {
return (
<div>
<h2>{props.name}</h2>
<p>{props.email}</p>
<span>{props.role}</span>
</div>
);
}
function App() {
return <UserCard name="Alice" email="alice@example.com" role="Admin" />;
}
Props can be any JavaScript value. Strings are written with quotes; everything else uses braces:
<UserCard name="Alice" age={30} active={true} tags={["a", "b"]} />
The distinction matters: age="30" passes the string "30", while age={30} passes the number 30. For most rendering this does not matter, but for arithmetic or comparisons it does.
b. Reading props with destructuring
Destructuring in the function parameter is the idiomatic way to read props. It makes the component’s expected props explicit and avoids repeating props. throughout the body.
function UserCard({ name, email, role }) {
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
<span>{role}</span>
</div>
);
}
Destructuring also allows renaming and default values:
function UserCard({ name, email, role = "User" }) {
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
<span>{role}</span>
</div>
);
}
When a component needs to access the full props object — to pass remaining props through, or to log them — destructure in the body with the rest operator:
function Button({ label, variant = "primary", ...rest }) {
return (
<button className={`btn btn-${variant}`} {...rest}>
{label}
</button>
);
}
The rest object contains all props not destructured. This pattern is common for wrapper components that forward DOM attributes.
c. Default values
Default values are written in the destructuring pattern:
function Greeting({ name, greeting = "Hello" }) {
return <h1>{greeting}, {name}!</h1>;
}
When greeting is not passed, it defaults to "Hello". When it is passed as undefined, the default applies. When it is passed as null, the default does not apply — null is a value.
<Greeting name="Alice" /> // greeting = "Hello"
<Greeting name="Alice" greeting={undefined} /> // greeting = "Hello"
<Greeting name="Alice" greeting={null} /> // greeting = null
The older defaultProps mechanism is deprecated for function components. The destructuring default is the modern approach and is type-safe in TypeScript.
d. The children prop
The children prop is special: it contains the JSX between a component’s opening and closing tags.
function Card({ title, children }) {
return (
<div className="card">
<h3>{title}</h3>
<div className="card-body">{children}</div>
</div>
);
}
<Card title="Profile">
<p>Content goes here</p>
<button>Action</button>
</Card>
Everything between <Card> and </Card> becomes props.children. This is how components wrap arbitrary content: modals, layouts, tooltips, and containers all use children.
children can be a single element, multiple elements, a string, a number, or a function. React treats it as a special prop and provides utilities like React.Children.map for iterating over it, though direct rendering ({children}) is more common.
Named slots extend this pattern:
function Layout({ header, sidebar, children }) {
return (
<div className="layout">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}
<Layout header={<h1>Title</h1>} sidebar={<Nav />}>
<Content />
</Layout>
Props that accept elements allow multiple insertion points, which is more flexible than children alone.
e. TypeScript prop types
In TypeScript, props are typed with an interface or type alias. The component’s parameter is annotated with that type.
interface UserCardProps {
name: string;
email: string;
role?: string;
onSelect?: (id: string) => void;
}
function UserCard({ name, email, role = "User", onSelect }: UserCardProps) {
return (
<div onClick={() => onSelect?.(name)}>
<h2>{name}</h2>
<p>{email}</p>
<span>{role}</span>
</div>
);
}
The ? marks optional props. Required props must be provided by the caller, and TypeScript reports an error if they are missing or the wrong type.
For components with children, the type is React.ReactNode:
interface CardProps {
title: string;
children: React.ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<div>
<h3>{title}</h3>
{children}
</div>
);
}
For wrapper components that spread DOM attributes, extend the appropriate HTML element props:
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary";
}
function Button({ variant = "primary", children, ...rest }: ButtonProps) {
return (
<button className={`btn btn-${variant}`} {...rest}>
{children}
</button>
);
}
This gives the component all the standard button attributes (onClick, disabled, type) plus the custom variant prop.
f. The spread operator
The spread operator passes an object’s properties as individual props:
const userProps = { name: "Alice", email: "alice@example.com", role: "Admin" };
<UserCard {...userProps} />
// equivalent to
<UserCard name="Alice" email="alice@example.com" role="Admin" />
This is useful when the props are already collected in an object, or when forwarding props to a child component. It is also used to pass through unknown props in wrapper components.
function Input({ label, ...inputProps }) {
return (
<label>
{label}
<input {...inputProps} />
</label>
);
}
A caveat: spread passes all enumerable properties, including ones the child may not expect. In TypeScript, the spread is type-checked, and unexpected props produce an error unless the child’s props type allows them.
g. Callback props
Props flow down, but events flow up through callback props. A parent passes a function; the child calls it when something happens.
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
function App() {
const handleClick = () => console.log("clicked");
return <Button label="Click" onClick={handleClick} />;
}
The parent defines the behavior; the child invokes it. The child does not know what the callback does, only that it should call it in response to an event. This decouples the child from the parent’s logic.
Callback props often pass data upward:
function UserList({ users, onSelect }) {
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onSelect(user.id)}>
{user.name}
</li>
))}
</ul>
);
}
The child calls onSelect with the selected user’s ID, and the parent decides what to do with it. This is the standard pattern for lifting state: the parent owns the state, the child reports events.
A common pitfall is passing a new function on every render, which can cause unnecessary re-renders in memoized children. Wrapping the callback in useCallback stabilizes it when the dependency array is correct:
const handleSelect = useCallback((id) => {
setSelectedId(id);
}, []);
This matters for performance in large lists, but is not needed everywhere.
Complete Example Session
// ============================================
// PART 1: PASSING A SINGLE PROP
// ============================================
function Welcome({ name }) {
return <h1>Hello, {name}!</h1>;
}
// Usage
<Welcome name="Alice" />
// ============================================
// PART 2: PASSING MULTIPLE PROPS
// ============================================
function UserCard({ name, email, role }) {
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
<span>{role}</span>
</div>
);
}
// Usage
<UserCard name="Alice" email="alice@example.com" role="Admin" />
// ============================================
// PART 3: NON-STRING PROPS
// ============================================
function Product({ name, price, inStock, tags }) {
return (
<div>
<h3>{name}</h3>
<p>${price.toFixed(2)}</p>
<p>{inStock ? "In stock" : "Out of stock"}</p>
<ul>{tags.map(t => <li key={t}>{t}</li>)}</ul>
</div>
);
}
// Usage
<Product name="Widget" price={19.99} inStock={true} tags={["new", "sale"]} />
// ============================================
// PART 4: DEFAULT VALUES
// ============================================
function Greeting({ name, greeting = "Hello" }) {
return <h1>{greeting}, {name}!</h1>;
}
// greeting defaults to "Hello"
<Greeting name="Alice" />
// greeting overridden
<Greeting name="Bob" greeting="Hi" />
// ============================================
// PART 5: THE children PROP
// ============================================
function Card({ title, children }) {
return (
<div className="card">
<h3>{title}</h3>
<div>{children}</div>
</div>
);
}
// Usage
<Card title="Profile">
<p>Any content here</p>
</Card>
// ============================================
// PART 6: NAMED SLOTS
// ============================================
function Layout({ header, sidebar, children }) {
return (
<div>
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}
// Usage
<Layout header={<h1>Title</h1>} sidebar={<Nav />}>
<Content />
</Layout>
// ============================================
// PART 7: CALLBACK PROP
// ============================================
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
function App() {
const handleSave = () => console.log("saving");
return <Button label="Save" onClick={handleSave} />;
}
// ============================================
// PART 8: CALLBACK WITH DATA
// ============================================
function UserList({ users, onSelect }) {
return (
<ul>
{users.map(u => (
<li key={u.id} onClick={() => onSelect(u.id)}>{u.name}</li>
))}
</ul>
);
}
function App() {
const [selected, setSelected] = useState(null);
return <UserList users={users} onSelect={setSelected} />;
}
// ============================================
// PART 9: TYPESCRIPT PROPS
// ============================================
interface ProductCardProps {
name: string;
price: number;
onAddToCart: () => void;
imageUrl?: string;
}
function ProductCard({ name, price, onAddToCart, imageUrl }: ProductCardProps) {
return (
<div>
{imageUrl && <img src={imageUrl} alt={name} />}
<h3>{name}</h3>
<p>${price.toFixed(2)}</p>
<button onClick={onAddToCart}>Add</button>
</div>
);
}
// ============================================
// PART 10: SPREAD AND REST PROPS
// ============================================
function Input({ label, ...inputProps }) {
return (
<label>
{label}
<input {...inputProps} />
</label>
);
}
// Any standard input attribute passes through
<Input label="Email" type="email" placeholder="you@example.com" required />
These ten parts cover the full range of prop usage: single and multiple props, non-string values, defaults, children, named slots, callbacks, callbacks with data, TypeScript types, and spread/rest props. Each pattern is idiomatic modern React.
Quick Reference
Passing Props
| Value Type | Syntax | Example |
|---|---|---|
| String | attr="text" | name="Alice" |
| Number | attr={42} | age={30} |
| Boolean | attr={true} or bare | disabled |
| Array | attr={[...]} | tags={["a", "b"]} |
| Object | attr={{...}} | style={{ color: "red" }} |
| Function | attr={fn} | onClick={handleClick} |
| Element | attr={<El />} | header={<h1>Hi</h1>} |
| Spread | {...obj} | {...userProps} |
Reading Props
| Pattern | Syntax |
|---|---|
| Access object | props.name |
| Destructure | { name } |
| Default value | { name = "Guest" } |
| Rename | { name: userName } |
| Rest | { a, ...rest } |
| Children | { children } |
| Whole object | props |
Prop Flow Direction
| Direction | Mechanism |
|---|---|
| Parent to child | Props |
| Child to parent | Callback props |
| Sibling to sibling | Lift state to common ancestor |
TypeScript Prop Types
| Pattern | Type |
|---|---|
| Required | name: string |
| Optional | role?: string |
| Callback | onClick: () => void |
| Children | children: React.ReactNode |
| HTML extension | extends React.ButtonHTMLAttributes<...> |
Best Practices
✅ Do This:
function Card({ title, children, variant = "default" }) { // Destructure, defaults
return <div className={`card card-${variant}`}>{children}</div>;
}
function Button({ onClick, children }) { // Callback prop
return <button onClick={onClick}>{children}</button>;
}
function Input({ label, ...rest }) { // Rest for forwarding
return <label>{label}<input {...rest} /></label>;
}
// TypeScript
interface Props { name: string; onSave: () => void; }
❌ Don’t Do This:
function Card(props) {
props.title = "New"; // ❌ Mutating props
return <div>{props.title}</div>;
}
function Button({ onClick = () => {} }) { // ❌ Silent no-op
return <button onClick={onClick}>Click</button>;
}
<UserCard name="Alice" email={undefined} role={null} /> // ❌ Unclear intent
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Prop is undefined | Not passed or misspelled | Check JSX attribute name |
| String instead of number | age="30" instead of age={30} | Use braces for non-strings |
| Default not applied | Passing null instead of omitting | Omit the prop or pass undefined |
| Cannot mutate prop | Props are read-only | Lift state or use a callback |
| Callback not called | Handler not passed | Verify the prop is provided |
| Child re-renders constantly | New function on every render | Wrap in useCallback |
| Spread passes unwanted props | Too many properties in object | Destructure and pass explicitly |
Real-World Examples
1. Simple Text Prop
<Button label="Save" />
2. Number Prop with Braces
<ProgressBar value={75} max={100} />
3. Boolean Prop
<Input disabled required />
4. Object Prop
<UserCard user={{ name: "Alice", email: "a@example.com" }} />
5. Array Prop
<TagList tags={["react", "jsx", "hooks"]} />
6. Callback Prop
<Button onClick={() => setCount(c => c + 1)}>Increment</Button>
7. Callback with Argument
<ItemRow item={item} onDelete={() => deleteItem(item.id)} />
8. Children Prop
<Modal><p>Modal content</p></Modal>
9. Named Slot Prop
<Layout header={<Header />} footer={<Footer />}>
<Main />
</Layout>
10. Spread Props
const inputProps = { type: "email", placeholder: "Email", required: true };
<Input label="Email" {...inputProps} />
Visual
Prop Flow: Parent to Child
┌──────────────────────────────────────────────────────────────┐
│ ONE-WAY DATA FLOW │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Parent │ │
│ │ const user = { name: "Alice", role: "Admin" }; │ │
│ │ <UserCard user={user} onSelect={handleSelect} /> │ │
│ └──────────────────────┬─────────────────────────────────┘ │
│ │ │
│ props flow │ callbacks flow │
│ down │ up │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Child (UserCard) │ │
│ │ function UserCard({ user, onSelect }) { │ │
│ │ return <div onClick={() => onSelect(user.name)}> │ │
│ │ {user.name} │ │
│ │ </div>; │ │
│ │ } │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Data goes down as props; events go up as callback calls. │
│ The child never modifies the parent's data directly. │
└──────────────────────────────────────────────────────────────┘
Props vs State
┌──────────────────────────────────────────────────────────────┐
│ PROPS vs STATE │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Owned by: parent │ │ Owned by: component │ │
│ │ Read-only: yes │ │ Mutable: via setter │ │
│ │ Passed: as JSX attr │ │ Declared: useSt │ │
│ │ Changes: parent │ │ Changes: component │ │
│ │ re-renders │ │ re-renders │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ A component cannot change its props. │
│ To change data, either the parent changes the prop value │
│ (and the child re-renders), or the component owns the data │
│ as state and changes it itself. │
└──────────────────────────────────────────────────────────────┘
children vs Named Slots
┌──────────────────────────────────────────────────────────────┐
│ CHILDREN PROP │
│ │
│ <Card title="Profile"> │
│ <p>Content</p> │
│ </Card> │
│ │ │
│ └── props.children = <p>Content</p> │
│ │
│ NAMED SLOTS │
│ │
│ <Layout header={<h1>Title</h1>} sidebar={<Nav />}> │
│ <Content /> │
│ </Layout> │
│ │ │
│ ├── props.header = <h1>Title</h1> │
│ ├── props.sidebar = <Nav /> │
│ └── props.children = <Content /> │
│ │
│ Named slots give multiple insertion points; children gives │
│ one. Both are props; the names are conventions. │
└──────────────────────────────────────────────────────────────┘
Rest and Spread Props
┌──────────────────────────────────────────────────────────────┐
│ FORWARDING PROPS WITH REST AND SPREAD │
│ │
│ function Input({ label, ...rest }) { │
│ return ( │
│ <label> │
│ {label} │
│ <input {...rest} /> │
│ </label> │
│ ); │
│ } │
│ │
│ Usage: │
│ <Input label="Email" │
│ type="email" │
│ placeholder="you@example.com" │
│ required /> │
│ │
│ label = "Email" │
│ rest = { type: "email", placeholder: "...", required: true }│
│ │
│ The <input> receives all standard attributes without the │
│ component listing each one. This is how wrapper components │
│ stay flexible. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Props | Read-only data passed from parent to child |
| Passing | JSX attributes: name="Alice", age={30} |
| Reading | Destructure in parameter: { name, age } |
| Defaults | Destructuring defaults: { role = "User" } |
| children | Special prop for content between tags |
| Named slots | Props that accept React elements |
| Callback props | Functions passed down, called by child |
| Spread | {...obj} passes object properties as props |
| Rest | { a, ...rest } collects remaining props |
| TypeScript | Interface for props; React.ReactNode for children |
| Data flow | Down via props, up via callbacks |
Key takeaways:
- Props flow one way: parent to child. The parent provides data; the child renders it. The child cannot modify its props.
- Props are read-only. Mutating a prop is a bug. To change data, lift the state to a common ancestor or use a callback.
- Destructuring is the idiomatic way to read props.
function Card({ title, children })makes the expected props explicit. - Default values go in the destructuring.
{ role = "User" }replaces the deprecateddefaultProps. childrenis a special prop. It contains the JSX between a component’s tags and is the primary mechanism for composition.- Named slots extend
children. Props that accept elements allow multiple insertion points, which is more flexible than a single children slot. - Callbacks send data upward. When a child needs to communicate an event, the parent passes a function, and the child calls it with the data.
- Rest and spread forward props.
{ label, ...rest }collects remaining props;{...rest}passes them to a child element. This keeps wrapper components flexible. - TypeScript types catch prop errors. An interface for props ensures that required props are provided and types match.
Remember: Props are the interface between components. They define what a component needs to render and how it communicates with its parent. Passing props down and calling callbacks up is the fundamental pattern of React data flow. The child is a pure function of its props: given the same props, it renders the same output, which makes it easy to test and reason about. When a component needs to change data, the change happens in the parent — either by the parent updating its own state, or by the child calling a callback the parent provided. This one-directional flow is what makes React applications predictable. Understanding props is understanding how React applications are assembled from components.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!