| |

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 TypeSyntaxExample
Stringattr="text"name="Alice"
Numberattr={42}age={30}
Booleanattr={true} or baredisabled
Arrayattr={[...]}tags={["a", "b"]}
Objectattr={{...}}style={{ color: "red" }}
Functionattr={fn}onClick={handleClick}
Elementattr={<El />}header={<h1>Hi</h1>}
Spread{...obj}{...userProps}

Reading Props

PatternSyntax
Access objectprops.name
Destructure{ name }
Default value{ name = "Guest" }
Rename{ name: userName }
Rest{ a, ...rest }
Children{ children }
Whole objectprops

Prop Flow Direction

DirectionMechanism
Parent to childProps
Child to parentCallback props
Sibling to siblingLift state to common ancestor

TypeScript Prop Types

PatternType
Requiredname: string
Optionalrole?: string
CallbackonClick: () => void
Childrenchildren: React.ReactNode
HTML extensionextends 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

PitfallWhy It HappensFix
Prop is undefinedNot passed or misspelledCheck JSX attribute name
String instead of numberage="30" instead of age={30}Use braces for non-strings
Default not appliedPassing null instead of omittingOmit the prop or pass undefined
Cannot mutate propProps are read-onlyLift state or use a callback
Callback not calledHandler not passedVerify the prop is provided
Child re-renders constantlyNew function on every renderWrap in useCallback
Spread passes unwanted propsToo many properties in objectDestructure 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

ItemValue
PropsRead-only data passed from parent to child
PassingJSX attributes: name="Alice", age={30}
ReadingDestructure in parameter: { name, age }
DefaultsDestructuring defaults: { role = "User" }
childrenSpecial prop for content between tags
Named slotsProps that accept React elements
Callback propsFunctions passed down, called by child
Spread{...obj} passes object properties as props
Rest{ a, ...rest } collects remaining props
TypeScriptInterface for props; React.ReactNode for children
Data flowDown 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 deprecated defaultProps.
  • children is 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!