Episode 2 — React Frontend Architecture NextJS / 2.2 — React Components and Props

2.2.b — Understanding Props

In one sentence: Props are the mechanism React uses to pass read-only data from parent components to child components, enabling dynamic and reusable UI.

Navigation: ← 2.2.a — Functional Components · Next → 2.2.c — Dynamic Rendering


Table of Contents

  1. What Are Props?
  2. How Props Flow — The Water Analogy
  3. Passing Props to Components
  4. The Props Object
  5. Destructuring Props
  6. Default Props
  7. The Children Prop
  8. Spreading Props
  9. Props Are Read-Only (Immutability)
  10. Prop Types and Runtime Validation
  11. Props with TypeScript
  12. Functions as Props (Callbacks)
  13. Common Prop Patterns in Real Applications
  14. Prop Drilling — The Problem
  15. Anti-Patterns and Mistakes
  16. Key Takeaways
  17. Explain-It Challenge

1. What Are Props?

Props (short for "properties") are the inputs to a React component. They are how data flows from a parent component down to a child component. If components are functions, props are the arguments you pass to those functions.

┌──────────────────────────────────────────────────────────┐
│                    FUNCTION ANALOGY                       │
│                                                          │
│   Regular function:                                      │
│   greet("Alice", 25)                                     │
│         ↑       ↑                                        │
│       arg1    arg2                                       │
│                                                          │
│   React component:                                       │
│   <UserCard name="Alice" age={25} />                     │
│              ↑            ↑                               │
│            prop1        prop2                             │
│                                                          │
│   Both receive inputs and produce output.                │
│   Functions return values. Components return JSX.        │
└──────────────────────────────────────────────────────────┘

Why Props Matter

Without props, every component would be static — hardcoded content with no way to customize behavior. Props enable:

CapabilityWithout PropsWith Props
ReusabilityEvery card is identicalEach card shows different data
CustomizationOne fixed button stylePrimary, secondary, danger variants
CompositionRigid page layoutsFlexible, composable sections
Data flowNo parent-child communicationParent controls child behavior
TestingCan't test variationsPass different props to test edge cases

The Simplest Example

// Parent component passes props
function App() {
  return <Greeting name="Alice" />;
}

// Child component receives props
function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

// Renders: <h1>Hello, Alice!</h1>

Notice three things:

  1. Props look like HTML attributesname="Alice" resembles class="container"
  2. Strings use quotes, everything else uses curly braces — age={25}, active={true}
  3. Props are accessed as object propertiesprops.name, props.age

2. How Props Flow — The Water Analogy

React enforces one-way data flow (also called "unidirectional data flow"). Props can only flow downward — from parent to child, never the other way around.

┌─────────────────────────────────────────────────────┐
│              ONE-WAY DATA FLOW                       │
│                                                     │
│         ┌──────────┐                                │
│         │   App     │  ← Data originates here       │
│         │ (Parent)  │                                │
│         └────┬──────┘                                │
│              │                                       │
│         props flow DOWN (like gravity)               │
│              │                                       │
│     ┌───────┴────────┐                               │
│     ▼                ▼                               │
│  ┌──────┐      ┌──────────┐                          │
│  │Header│      │  Main    │                          │
│  │(child)│     │ (child)  │                          │
│  └──────┘      └────┬─────┘                          │
│                     │                                │
│                props flow DOWN                       │
│                     │                                │
│              ┌──────┴──────┐                         │
│              ▼             ▼                          │
│         ┌────────┐   ┌────────┐                      │
│         │  Card  │   │  Card  │                      │
│         │(grand- │   │(grand- │                      │
│         │ child) │   │ child) │                      │
│         └────────┘   └────────┘                      │
│                                                     │
│  ✗ Children CANNOT send props UP to parents         │
│  ✗ Siblings CANNOT send props to each other         │
│  ✓ Parents CAN pass callbacks (functions) as props  │
│    so children can "communicate back"               │
└─────────────────────────────────────────────────────┘

Why One-Way?

AspectOne-Way (React)Two-Way (e.g., Angular v1)
DebuggingEasy — trace data source upwardHard — changes come from anywhere
PredictabilityHigh — parent controls childLow — child can mutate parent state
PerformanceEfficient — clear render pathsComplex — digest cycles needed
Mental modelSimple — follow the treeComplex — circular dependencies

Think of it like a water system:

  • Parent = Water tower (source)
  • Props = Pipes (transport)
  • Children = Faucets (consumers)

Water flows down through pipes. Faucets can't push water back up to the tower. But faucets can signal the tower (via callback props) to change the water pressure.


3. Passing Props to Components

Passing Different Data Types

function App() {
  const user = { name: "Alice", role: "admin" };
  const hobbies = ["reading", "coding", "hiking"];

  return (
    <UserProfile
      {/* String */}
      name="Alice"
      
      {/* Number */}
      age={28}
      
      {/* Boolean */}
      isActive={true}
      
      {/* Boolean shorthandjust the name means true */}
      verified
      
      {/* Array */}
      hobbies={hobbies}
      
      {/* Object */}
      user={user}
      
      {/* Function */}
      onEdit={() => console.log("Edit clicked")}
      
      {/* JSX / React element */}
      icon={<StarIcon />}
      
      {/* Null / Undefined (won't render) */}
      subtitle={null}
      
      {/* Expression */}
      displayName={user.name.toUpperCase()}
      
      {/* Template literal */}
      greeting={`Hello, ${user.name}!`}
    />
  );
}

The Boolean Shorthand

When a prop is true, you can omit the value:

// These are identical:
<Button disabled={true} />
<Button disabled />

// To pass false, you MUST be explicit:
<Button disabled={false} />

// Or simply don't include it (undefined is falsy):
<Button />

Dynamic Props from Variables

function ProductPage() {
  const product = {
    id: 1,
    name: "Wireless Headphones",
    price: 79.99,
    inStock: true,
    rating: 4.5,
    reviews: 128,
  };

  return (
    <div className="product-page">
      <ProductImage src={product.image} alt={product.name} />
      <ProductInfo
        name={product.name}
        price={product.price}
        rating={product.rating}
        reviewCount={product.reviews}
      />
      <AddToCartButton
        productId={product.id}
        available={product.inStock}
      />
    </div>
  );
}

Conditional Props

function App() {
  const isLoggedIn = true;
  const userName = "Alice";

  return (
    <Navbar
      // Conditional proponly pass if truthy
      userName={isLoggedIn ? userName : undefined}
      
      // Conditional object spread
      {...(isLoggedIn && { dashboardLink: "/dashboard" })}
    />
  );
}

4. The Props Object

Every functional component receives exactly one argument: the props object. This is true whether you pass 0 props or 50.

// React internally calls your component like this:
// Greeting({ name: "Alice", age: 28 })

function Greeting(props) {
  console.log(props);
  // Output: { name: "Alice", age: 28 }
  
  console.log(typeof props);
  // Output: "object"
  
  return <h1>Hello, {props.name}! You are {props.age}.</h1>;
}

// Usage
<Greeting name="Alice" age={28} />

What the Props Object Contains

function DebugComponent(props) {
  // See ALL props
  console.log("All props:", props);
  
  // Check what keys exist
  console.log("Keys:", Object.keys(props));
  
  // Check if a specific prop was passed
  console.log("Has name:", "name" in props);
  console.log("Has age:", props.age !== undefined);
  
  return <pre>{JSON.stringify(props, null, 2)}</pre>;
}

// Usage
<DebugComponent 
  name="Alice" 
  age={28} 
  isActive 
  tags={["a", "b"]} 
/>

// props object:
// {
//   name: "Alice",
//   age: 28,
//   isActive: true,
//   tags: ["a", "b"]
// }

Props You Didn't Pass Are undefined

function UserCard(props) {
  console.log(props.name);     // "Alice"
  console.log(props.email);    // undefined (not passed)
  console.log(props.avatar);   // undefined (not passed)
  
  return (
    <div>
      <h2>{props.name}</h2>
      {/* undefined renders as nothing — no error */}
      <p>{props.email}</p>
      {/* But accessing deep properties on undefined WILL crash */}
      {/* <p>{props.address.street}</p>  💥 TypeError! */}
    </div>
  );
}

<UserCard name="Alice" />

Props vs State — Critical Distinction

┌──────────────────────────────────────────────────────┐
│              PROPS vs STATE                           │
│                                                      │
│  ┌──────────────┐          ┌──────────────┐          │
│  │    PROPS     │          │    STATE     │          │
│  ├──────────────┤          ├──────────────┤          │
│  │ Passed IN    │          │ Created IN   │          │
│  │ from parent  │          │ the component│          │
│  │              │          │ itself       │          │
│  │ Read-only    │          │ Mutable      │          │
│  │ (immutable)  │          │ (via setter) │          │
│  │              │          │              │          │
│  │ Controlled   │          │ Controlled   │          │
│  │ by PARENT    │          │ by SELF      │          │
│  │              │          │              │          │
│  │ Like function│          │ Like local   │          │
│  │ arguments    │          │ variables    │          │
│  └──────────────┘          └──────────────┘          │
│                                                      │
│  A component re-renders when EITHER changes.         │
└──────────────────────────────────────────────────────┘
FeaturePropsState
Who owns it?Parent componentThe component itself
Can component change it?No (read-only)Yes (via setState)
Triggers re-render?Yes, when parent passes new valuesYes, when updated
Initial sourceParent's render outputuseState() initial value
Used forConfiguration, data passingInteractive data, UI toggles

5. Destructuring Props

The most common pattern in production React is destructuring — extracting values from the props object directly in the function signature.

Basic Destructuring

// WITHOUT destructuring (verbose)
function UserCard(props) {
  return (
    <div>
      <h2>{props.name}</h2>
      <p>{props.email}</p>
      <span>{props.role}</span>
    </div>
  );
}

// WITH destructuring (clean, preferred)
function UserCard({ name, email, role }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>{email}</p>
      <span>{role}</span>
    </div>
  );
}

Destructuring with Defaults

function Button({ 
  label, 
  variant = "primary",    // default: "primary"
  size = "medium",        // default: "medium"
  disabled = false,       // default: false
  onClick 
}) {
  return (
    <button 
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {label}
    </button>
  );
}

// All defaults applied:
<Button label="Click me" onClick={handleClick} />
// variant="primary", size="medium", disabled=false

// Override specific defaults:
<Button label="Delete" variant="danger" size="large" onClick={handleDelete} />

Destructuring with Rest Operator

// Capture specific props, forward everything else
function Input({ label, error, ...restProps }) {
  return (
    <div className="form-field">
      <label>{label}</label>
      {/* Spread remaining props onto the native input */}
      <input {...restProps} />
      {error && <span className="error">{error}</span>}
    </div>
  );
}

// Usage — type, placeholder, onChange are forwarded to <input>
<Input 
  label="Email"
  error="Invalid email"
  type="email"
  placeholder="you@example.com"
  onChange={handleChange}
  autoComplete="email"
/>

Renaming During Destructuring

function UserProfile({ 
  name: userName,           // rename "name" to "userName"
  avatar: profileImage,     // rename "avatar" to "profileImage"
  isActive: online          // rename "isActive" to "online"
}) {
  return (
    <div>
      <img src={profileImage} alt={userName} />
      <h2>{userName}</h2>
      {online && <span className="badge">Online</span>}
    </div>
  );
}

Nested Destructuring

// When props contain objects
function OrderSummary({ 
  order: { id, total, items },           // destructure nested object
  customer: { name, address: { city } }  // deep nesting
}) {
  return (
    <div>
      <h2>Order #{id}</h2>
      <p>Customer: {name} from {city}</p>
      <p>Items: {items.length}</p>
      <p>Total: ${total.toFixed(2)}</p>
    </div>
  );
}

// Usage
<OrderSummary 
  order={{ id: 1001, total: 59.99, items: ["shirt", "pants"] }}
  customer={{ name: "Alice", address: { city: "NYC", zip: "10001" } }}
/>

Warning: Deep nested destructuring can make code hard to read. If you're going more than 2 levels deep, consider destructuring in the function body instead.


6. Default Props

There are three ways to provide default values for props:

Method 1: Default Parameters (Recommended)

function Avatar({ 
  src = "/images/default-avatar.png", 
  size = 48, 
  alt = "User avatar" 
}) {
  return (
    <img 
      src={src} 
      alt={alt} 
      width={size} 
      height={size} 
      style={{ borderRadius: "50%" }}
    />
  );
}

// No props — all defaults applied
<Avatar />
// Renders with src="/images/default-avatar.png", size=48

// Partial override
<Avatar src="/alice.jpg" size={64} />
// Renders with src="/alice.jpg", size=64, alt="User avatar"

Method 2: defaultProps (Legacy — Avoid in New Code)

function Avatar({ src, size, alt }) {
  return (
    <img src={src} alt={alt} width={size} height={size} />
  );
}

Avatar.defaultProps = {
  src: "/images/default-avatar.png",
  size: 48,
  alt: "User avatar",
};

Why avoid defaultProps? React team has deprecated defaultProps for function components. It's only still supported for class components. Default parameters are the standard approach.

Method 3: Logical OR / Nullish Coalescing in the Body

function Avatar(props) {
  // OR operator (treats "", 0, false as falsy — careful!)
  const src = props.src || "/images/default-avatar.png";
  
  // Nullish coalescing (only null/undefined trigger default — safer)
  const size = props.size ?? 48;
  const alt = props.alt ?? "User avatar";
  
  return <img src={src} alt={alt} width={size} height={size} />;
}

Comparison Table

MethodHandles undefinedHandles nullHandles ""Handles 0Recommended?
Default paramsYes → defaultNo → nullNo → ""No → 0Yes
defaultPropsYes → defaultNo → nullNo → ""No → 0No (deprecated)
|| operatorYes → defaultYes → defaultYes → defaultYes → defaultRisky
?? operatorYes → defaultYes → defaultNo → ""No → 0Good alternative

7. The Children Prop

The children prop is a special built-in prop that contains whatever you put between a component's opening and closing tags.

Basic Usage

function Card({ children }) {
  return <div className="card">{children}</div>;
}

// String children
<Card>Hello World</Card>
// children = "Hello World"

// JSX children
<Card>
  <h2>Title</h2>
  <p>Description here</p>
</Card>
// children = [<h2>Title</h2>, <p>Description here</p>]

// Mixed children
<Card>
  Text node
  <span>Element node</span>
  {42}
</Card>

Wrapper / Layout Components

The children prop is the foundation of composition in React:

function PageLayout({ children }) {
  return (
    <div className="page">
      <Header />
      <main className="content">{children}</main>
      <Footer />
    </div>
  );
}

function Modal({ title, children, onClose }) {
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h2>{title}</h2>
          <button onClick={onClose}>×</button>
        </div>
        <div className="modal-body">{children}</div>
      </div>
    </div>
  );
}

// Usage — any content can go inside
<Modal title="Confirm Delete" onClose={handleClose}>
  <p>Are you sure you want to delete this item?</p>
  <p>This action cannot be undone.</p>
  <div className="actions">
    <Button variant="danger" onClick={handleDelete}>Delete</Button>
    <Button variant="secondary" onClick={handleClose}>Cancel</Button>
  </div>
</Modal>

Children Can Be Anything

function Container({ children }) {
  return <div className="container">{children}</div>;
}

// String
<Container>Hello</Container>

// Number
<Container>{42}</Container>

// Array of elements
<Container>
  {[1, 2, 3].map(n => <span key={n}>{n}</span>)}
</Container>

// Another component
<Container>
  <UserCard name="Alice" />
</Container>

// Function (Render Prop pattern — advanced)
<Container>
  {(data) => <span>{data}</span>}
</Container>

// Nothing (renders empty container)
<Container />
<Container>{null}</Container>
<Container>{false}</Container>
<Container>{undefined}</Container>

Named Slots Pattern (Multiple Children Areas)

Unlike Vue or Svelte that have named slots, React uses multiple props:

function PageLayout({ header, sidebar, children, footer }) {
  return (
    <div className="layout">
      <header className="layout-header">{header}</header>
      <div className="layout-body">
        <aside className="layout-sidebar">{sidebar}</aside>
        <main className="layout-main">{children}</main>
      </div>
      <footer className="layout-footer">{footer}</footer>
    </div>
  );
}

// Usage
<PageLayout
  header={<Navbar />}
  sidebar={<SideMenu items={menuItems} />}
  footer={<FooterLinks />}
>
  <h1>Welcome to Dashboard</h1>
  <DashboardWidgets />
</PageLayout>

8. Spreading Props

The spread operator (...) lets you pass all properties of an object as individual props.

Basic Spread

const buttonProps = {
  label: "Submit",
  variant: "primary",
  size: "large",
  disabled: false,
  onClick: handleSubmit,
};

// Without spread — tedious
<Button 
  label={buttonProps.label}
  variant={buttonProps.variant}
  size={buttonProps.size}
  disabled={buttonProps.disabled}
  onClick={buttonProps.onClick}
/>

// With spread — clean
<Button {...buttonProps} />

// Both render identically

Spread with Override

const defaultCardProps = {
  variant: "outlined",
  padding: "medium",
  shadow: false,
};

// Spread first, then override specific props
<Card {...defaultCardProps} shadow={true} variant="elevated" />
// Result: variant="elevated", padding="medium", shadow=true

// ORDER MATTERS — last one wins
<Card shadow={true} {...defaultCardProps} />
// Result: variant="outlined", padding="medium", shadow=false
// (defaultCardProps.shadow=false overwrites shadow=true!)

Forwarding Props Pattern

// A common pattern: custom component wrapping a native element
function TextInput({ label, error, ...inputProps }) {
  return (
    <div className="field">
      {label && <label>{label}</label>}
      <input 
        className={`input ${error ? "input-error" : ""}`}
        {...inputProps}
      />
      {error && <span className="error-text">{error}</span>}
    </div>
  );
}

// All standard <input> props are forwarded automatically
<TextInput
  label="Username"
  error="Required"
  type="text"
  name="username"
  value={username}
  onChange={handleChange}
  placeholder="Enter username"
  maxLength={20}
  autoFocus
  required
/>

When NOT to Spread

// DANGER: Spreading user input or API data directly
const apiResponse = await fetch("/api/user").then(r => r.json());

// ❌ BAD — unknown props hit the DOM, causes warnings and security risks
<div {...apiResponse}>Content</div>

// ✅ GOOD — pick only what you need
<UserCard 
  name={apiResponse.name}
  email={apiResponse.email}
/>

9. Props Are Read-Only (Immutability)

This is React's cardinal rule for props: a component must never modify its own props.

What Immutability Means

// ❌ NEVER DO THIS — mutating props
function Greeting(props) {
  props.name = props.name.toUpperCase();  // 💥 VIOLATION!
  return <h1>Hello, {props.name}</h1>;
}

// ❌ NEVER DO THIS — mutating nested prop data
function UserList({ users }) {
  users.push({ name: "New User" });      // 💥 VIOLATION!
  users.sort((a, b) => a.name.localeCompare(b.name));  // 💥 VIOLATION!
  return users.map(u => <li key={u.id}>{u.name}</li>);
}

// ✅ DO THIS — create new data, leave props untouched
function Greeting({ name }) {
  const upperName = name.toUpperCase();   // New variable, props unchanged
  return <h1>Hello, {upperName}</h1>;
}

// ✅ DO THIS — copy before modifying
function UserList({ users }) {
  const sorted = [...users].sort((a, b) => a.name.localeCompare(b.name));
  return sorted.map(u => <li key={u.id}>{u.name}</li>);
}

Why Immutability?

┌──────────────────────────────────────────────────────────┐
│                WHY PROPS ARE IMMUTABLE                    │
│                                                          │
│  1. PREDICTABILITY                                       │
│     Parent passes name="Alice"                           │
│     → Child always sees "Alice"                          │
│     → No surprise mutations                              │
│                                                          │
│  2. PERFORMANCE                                          │
│     React compares prev props vs next props              │
│     If props didn't change → skip re-render              │
│     Mutation breaks this comparison                      │
│                                                          │
│  3. DEBUGGING                                            │
│     Data flows one way: parent → child                   │
│     If child could change props, data flow is circular   │
│     Circular flow = impossible to debug                  │
│                                                          │
│  4. PURE FUNCTIONS                                       │
│     Same props → same output (always)                    │
│     This makes components testable and composable        │
└──────────────────────────────────────────────────────────┘

The "Pure Function" Rule

React's documentation says: "All React components must act like pure functions with respect to their props."

// PURE — same input always produces same output
function Price({ amount, currency }) {
  const formatted = new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: currency,
  }).format(amount);
  
  return <span className="price">{formatted}</span>;
}

// Price({ amount: 29.99, currency: "USD" }) 
// ALWAYS renders: <span class="price">$29.99</span>

10. Prop Types and Runtime Validation

Before TypeScript was widespread, React provided PropTypes for runtime prop validation. It's still used in JavaScript projects.

Installation

npm install prop-types

Basic PropTypes

import PropTypes from "prop-types";

function UserCard({ name, age, email, role, avatar, onEdit }) {
  return (
    <div className="user-card">
      <img src={avatar} alt={name} />
      <h2>{name}</h2>
      <p>Age: {age}</p>
      <p>Email: {email}</p>
      <span className="badge">{role}</span>
      <button onClick={onEdit}>Edit</button>
    </div>
  );
}

UserCard.propTypes = {
  // Basic types
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
  email: PropTypes.string.isRequired,
  
  // Enum — one of specific values
  role: PropTypes.oneOf(["admin", "user", "moderator"]),
  
  // URL string (no built-in URL type, just string)
  avatar: PropTypes.string,
  
  // Function
  onEdit: PropTypes.func,
  
  // Any renderable content (string, number, element, array of these)
  children: PropTypes.node,
  
  // A React element specifically
  icon: PropTypes.element,
  
  // Object with specific shape
  address: PropTypes.shape({
    street: PropTypes.string,
    city: PropTypes.string.isRequired,
    zip: PropTypes.string,
  }),
  
  // Array of specific type
  tags: PropTypes.arrayOf(PropTypes.string),
  
  // Array of objects with shape
  orders: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      total: PropTypes.number.isRequired,
    })
  ),
  
  // One of several types
  id: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.number,
  ]),
};

UserCard.defaultProps = {
  age: 0,
  role: "user",
  avatar: "/default-avatar.png",
};

PropTypes Reference Table

ValidatorWhat It Checks
PropTypes.stringIs a string
PropTypes.numberIs a number
PropTypes.boolIs a boolean
PropTypes.funcIs a function
PropTypes.arrayIs an array
PropTypes.objectIs an object
PropTypes.symbolIs a symbol
PropTypes.nodeAnything renderable (string, number, element, array)
PropTypes.elementA React element
PropTypes.elementTypeA React component type (class or function)
PropTypes.instanceOf(Class)Instance of a class
PropTypes.oneOf([...])One of listed values
PropTypes.oneOfType([...])One of listed types
PropTypes.arrayOf(type)Array of specific type
PropTypes.objectOf(type)Object with values of specific type
PropTypes.shape({...})Object with specific shape
PropTypes.exact({...})Object with ONLY these keys
.isRequiredChain to make any prop required

Limitations of PropTypes

  1. Development only — Warnings only show in development mode, not production
  2. Runtime cost — Checks happen at runtime, not compile time
  3. No autocomplete — IDE can't use PropTypes for suggestions
  4. Easy to forget — No enforcement that you add them

This is why most modern projects prefer TypeScript instead.


11. Props with TypeScript

TypeScript provides compile-time prop validation, IDE autocomplete, and catch errors before your code runs.

Basic Interface

interface UserCardProps {
  name: string;
  age: number;
  email: string;
  role?: "admin" | "user" | "moderator";  // optional with union type
  avatar?: string;                          // optional
  onEdit?: () => void;                      // optional callback
}

function UserCard({ 
  name, 
  age, 
  email, 
  role = "user",      // default value
  avatar = "/default-avatar.png",
  onEdit 
}: UserCardProps) {
  return (
    <div className="user-card">
      <img src={avatar} alt={name} />
      <h2>{name}</h2>
      <p>Age: {age}</p>
      <p>Email: {email}</p>
      <span className="badge">{role}</span>
      {onEdit && <button onClick={onEdit}>Edit</button>}
    </div>
  );
}

Type vs Interface

// Interface — preferred for props (extendable)
interface ButtonProps {
  label: string;
  onClick: () => void;
}

// Type alias — also works
type ButtonProps = {
  label: string;
  onClick: () => void;
};

// Interface can extend
interface IconButtonProps extends ButtonProps {
  icon: React.ReactNode;
  iconPosition?: "left" | "right";
}

Common TypeScript Prop Patterns

// Children prop
interface LayoutProps {
  children: React.ReactNode;  // any renderable content
}

// Event handlers
interface FormProps {
  onSubmit: (data: FormData) => void;
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

// Forwarding HTML attributes
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
}
// Now InputProps includes ALL native <input> attributes plus label and error

// Generic component
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map(item => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

PropTypes vs TypeScript Comparison

FeaturePropTypesTypeScript
When errors appearRuntime (browser console)Compile time (editor/build)
IDE autocompleteNoYes
Bundle size impactAdds ~2KBZero (stripped at build)
Learning curveLowMedium
Complex typesLimitedFull type system
Required in 2026?Legacy projects onlyIndustry standard

12. Functions as Props (Callbacks)

Passing functions as props is how children communicate back to parents. This is React's answer to "how does a child tell its parent something happened?"

The Pattern

┌─────────────────────────────────────────────────────┐
│              CALLBACK PROP FLOW                      │
│                                                     │
│  ┌──────────────┐                                   │
│  │    Parent     │                                   │
│  │              │                                   │
│  │  state: count │──── passes function ────┐        │
│  │              │                          │        │
│  │  handleClick  │                          │        │
│  │  = () => {   │                          ▼        │
│  │    setCount  │                   ┌──────────┐    │
│  │    (c => c+1)│                   │  Child   │    │
│  │  }           │                   │          │    │
│  │              │  ◄── child calls ─│ onClick  │    │
│  │  re-renders  │     the function  │ ={prop}  │    │
│  │  with new    │                   │          │    │
│  │  count       │                   │ <button  │    │
│  └──────────────┘                   │  onClick │    │
│                                     │  ={onClick}>│  │
│                                     └──────────┘    │
└─────────────────────────────────────────────────────┘

Basic Callback

function Parent() {
  const [message, setMessage] = useState("");

  // Define the handler in the parent
  const handleMessage = (text) => {
    setMessage(text);
  };

  return (
    <div>
      <h1>Message: {message}</h1>
      {/* Pass function as prop */}
      <Child onSendMessage={handleMessage} />
    </div>
  );
}

function Child({ onSendMessage }) {
  return (
    <div>
      <button onClick={() => onSendMessage("Hello from Child!")}>
        Send Hello
      </button>
      <button onClick={() => onSendMessage("Goodbye from Child!")}>
        Send Goodbye
      </button>
    </div>
  );
}

Real-World Example — Todo App

function TodoApp() {
  const [todos, setTodos] = useState([
    { id: 1, text: "Learn React", done: false },
    { id: 2, text: "Build project", done: false },
  ]);

  const handleToggle = (id) => {
    setTodos(prev =>
      prev.map(todo =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  };

  const handleDelete = (id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  };

  const handleAdd = (text) => {
    setTodos(prev => [
      ...prev,
      { id: Date.now(), text, done: false },
    ]);
  };

  return (
    <div>
      <h1>Todos ({todos.filter(t => !t.done).length} remaining)</h1>
      <AddTodoForm onAdd={handleAdd} />
      <TodoList 
        todos={todos} 
        onToggle={handleToggle} 
        onDelete={handleDelete} 
      />
    </div>
  );
}

function TodoList({ todos, onToggle, onDelete }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={() => onToggle(todo.id)}
          onDelete={() => onDelete(todo.id)}
        />
      ))}
    </ul>
  );
}

function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li style={{ textDecoration: todo.done ? "line-through" : "none" }}>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={onToggle}
      />
      <span>{todo.text}</span>
      <button onClick={onDelete}>Delete</button>
    </li>
  );
}

function AddTodoForm({ onAdd }) {
  const [text, setText] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      onAdd(text.trim());
      setText("");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Add a todo..."
      />
      <button type="submit">Add</button>
    </form>
  );
}

Naming Convention for Callback Props

// Convention: on[Event] for the prop name
//             handle[Event] for the function definition

function Parent() {
  const handleClick = () => { /* ... */ };         // handle + Event
  const handleSubmit = (data) => { /* ... */ };
  const handleChange = (value) => { /* ... */ };
  const handleClose = () => { /* ... */ };

  return (
    <Child
      onClick={handleClick}         // on + Event
      onSubmit={handleSubmit}
      onChange={handleChange}
      onClose={handleClose}
    />
  );
}

13. Common Prop Patterns in Real Applications

Pattern 1: Variant Props

function Alert({ variant = "info", title, children, onDismiss }) {
  const styles = {
    info:    { bg: "#e3f2fd", border: "#2196f3", icon: "ℹ️" },
    success: { bg: "#e8f5e9", border: "#4caf50", icon: "✅" },
    warning: { bg: "#fff3e0", border: "#ff9800", icon: "⚠️" },
    error:   { bg: "#ffebee", border: "#f44336", icon: "❌" },
  };

  const style = styles[variant];

  return (
    <div style={{
      padding: "16px",
      backgroundColor: style.bg,
      borderLeft: `4px solid ${style.border}`,
      borderRadius: "4px",
      display: "flex",
      alignItems: "flex-start",
      gap: "12px",
    }}>
      <span>{style.icon}</span>
      <div style={{ flex: 1 }}>
        {title && <strong style={{ display: "block", marginBottom: "4px" }}>{title}</strong>}
        {children}
      </div>
      {onDismiss && (
        <button onClick={onDismiss} style={{ background: "none", border: "none", cursor: "pointer" }}>
          ×
        </button>
      )}
    </div>
  );
}

// Usage
<Alert variant="success" title="Saved!">Your changes have been saved.</Alert>
<Alert variant="error" onDismiss={handleDismiss}>Something went wrong.</Alert>
<Alert>This is a default info alert.</Alert>

Pattern 2: Render Prop (Function as Child)

function DataFetcher({ url, children }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  // Call children as a function, passing the state
  return children({ data, loading, error });
}

// Usage — parent controls the rendering
<DataFetcher url="/api/users">
  {({ data, loading, error }) => {
    if (loading) return <Spinner />;
    if (error) return <ErrorMessage error={error} />;
    return <UserList users={data} />;
  }}
</DataFetcher>

Pattern 3: Polymorphic "as" Prop

function Text({ as: Component = "p", children, ...props }) {
  return <Component {...props}>{children}</Component>;
}

// Renders different HTML elements based on "as" prop
<Text as="h1" className="title">Heading</Text>      // <h1>
<Text as="span" className="label">Label</Text>       // <span>
<Text as="label" htmlFor="email">Email</Text>        // <label>
<Text>Default paragraph</Text>                         // <p>

Pattern 4: Compound Component Props

function Tabs({ children, defaultTab }) {
  const [activeTab, setActiveTab] = useState(defaultTab);

  return (
    <div className="tabs">
      <div className="tab-list">
        {React.Children.map(children, child => (
          <button
            className={activeTab === child.props.name ? "active" : ""}
            onClick={() => setActiveTab(child.props.name)}
          >
            {child.props.label}
          </button>
        ))}
      </div>
      <div className="tab-panel">
        {React.Children.map(children, child =>
          child.props.name === activeTab ? child.props.children : null
        )}
      </div>
    </div>
  );
}

function Tab({ name, label, children }) {
  return <>{children}</>;
}

// Usage
<Tabs defaultTab="profile">
  <Tab name="profile" label="Profile">
    <ProfileForm />
  </Tab>
  <Tab name="settings" label="Settings">
    <SettingsPanel />
  </Tab>
  <Tab name="billing" label="Billing">
    <BillingInfo />
  </Tab>
</Tabs>

14. Prop Drilling — The Problem

Prop drilling happens when you pass props through multiple intermediate components that don't use the data themselves — they just forward it down.

┌─────────────────────────────────────────────────────┐
│                PROP DRILLING                         │
│                                                     │
│  ┌──────────┐                                       │
│  │   App     │  theme="dark"                        │
│  └────┬──────┘                                       │
│       │ passes theme                                 │
│  ┌────▼──────┐                                       │
│  │  Layout   │  theme="dark"  ← doesn't use it!     │
│  └────┬──────┘                                       │
│       │ forwards theme                               │
│  ┌────▼──────┐                                       │
│  │  Sidebar  │  theme="dark"  ← doesn't use it!     │
│  └────┬──────┘                                       │
│       │ forwards theme                               │
│  ┌────▼──────┐                                       │
│  │  Menu     │  theme="dark"  ← doesn't use it!     │
│  └────┬──────┘                                       │
│       │ forwards theme                               │
│  ┌────▼──────┐                                       │
│  │ MenuItem  │  theme="dark"  ← FINALLY uses it!    │
│  └───────────┘                                       │
│                                                     │
│  4 components touched, only 1 actually needs theme  │
└─────────────────────────────────────────────────────┘

The Problem in Code

// Every component must accept and forward `theme`
function App() {
  const theme = "dark";
  return <Layout theme={theme} />;
}

function Layout({ theme }) {
  // Layout doesn't USE theme, just passes it down
  return (
    <div>
      <Sidebar theme={theme} />
      <Main theme={theme} />
    </div>
  );
}

function Sidebar({ theme }) {
  // Sidebar doesn't USE theme, just passes it down
  return <Menu theme={theme} />;
}

function Menu({ theme }) {
  // Menu doesn't USE theme, just passes it down
  return (
    <ul>
      <MenuItem label="Home" theme={theme} />
      <MenuItem label="About" theme={theme} />
    </ul>
  );
}

function MenuItem({ label, theme }) {
  // FINALLY — someone actually uses `theme`!
  return (
    <li className={`menu-item menu-item-${theme}`}>
      {label}
    </li>
  );
}

Why It's a Problem

IssueImpact
VerbosityEvery intermediate component needs the prop in its signature
FragilityRename a prop → update every component in the chain
NoiseComponents carry props they don't care about
Refactoring painMoving components requires rewiring prop chains
Testing overheadMust provide drill-through props even in unit tests

Solutions (Preview — Covered in Later Topics)

  1. Context API (Topic 2.13) — Skip intermediate components entirely
  2. State management (Topic 2.13-2.14) — Zustand, Redux
  3. Component composition — Restructure to reduce nesting
  4. Render props / children — Pass pre-rendered content instead
// Solution preview: Composition reduces drilling
function App() {
  const theme = "dark";
  
  // Instead of drilling, pass the already-themed component
  return (
    <Layout
      sidebar={
        <Sidebar>
          <Menu>
            <MenuItem label="Home" theme={theme} />
            <MenuItem label="About" theme={theme} />
          </Menu>
        </Sidebar>
      }
    />
  );
}

15. Anti-Patterns and Mistakes

Mistake 1: Mutating Props

// ❌ WRONG — modifying the props array
function SortedList({ items }) {
  items.sort((a, b) => a.localeCompare(b));  // Mutates parent's array!
  return items.map(item => <li key={item}>{item}</li>);
}

// ✅ CORRECT — create a copy first
function SortedList({ items }) {
  const sorted = [...items].sort((a, b) => a.localeCompare(b));
  return sorted.map(item => <li key={item}>{item}</li>);
}

Mistake 2: Using Props to Initialize State (then ignoring updates)

// ❌ WRONG — state initialized from prop but never synced
function EditForm({ initialName }) {
  // If parent passes a new initialName, this component won't update!
  const [name, setName] = useState(initialName);
  return <input value={name} onChange={e => setName(e.target.value)} />;
}

// ✅ OPTION A — Use a key to force re-mount
<EditForm key={userId} initialName={user.name} />

// ✅ OPTION B — Use the prop directly (controlled component)
function EditForm({ name, onNameChange }) {
  return <input value={name} onChange={e => onNameChange(e.target.value)} />;
}

// ✅ OPTION C — Sync with useEffect (use sparingly)
function EditForm({ initialName }) {
  const [name, setName] = useState(initialName);
  useEffect(() => { setName(initialName); }, [initialName]);
  return <input value={name} onChange={e => setName(e.target.value)} />;
}

Mistake 3: Passing Inline Objects/Arrays (Unnecessary Re-renders)

// ❌ CAUSES RE-RENDER — new object reference every render
function App() {
  return (
    <UserCard 
      style={{ color: "red", fontSize: 16 }}   // New object each render!
      tags={["admin", "active"]}                // New array each render!
    />
  );
}

// ✅ STABLE REFERENCES — defined outside or memoized
const cardStyle = { color: "red", fontSize: 16 };
const adminTags = ["admin", "active"];

function App() {
  return <UserCard style={cardStyle} tags={adminTags} />;
}

Mistake 4: Too Many Props (God Component)

// ❌ TOO MANY PROPS — component does too much
<UserCard
  name={name} email={email} phone={phone} avatar={avatar}
  address={address} city={city} state={state} zip={zip}
  role={role} department={department} manager={manager}
  startDate={startDate} salary={salary} isActive={isActive}
  onEdit={onEdit} onDelete={onDelete} onPromote={onPromote}
/>

// ✅ BREAK INTO SMALLER COMPONENTS
<UserCard user={user}>
  <UserContactInfo email={email} phone={phone} />
  <UserAddress address={address} />
  <UserEmployment role={role} department={department} />
  <UserActions onEdit={onEdit} onDelete={onDelete} />
</UserCard>

Mistake 5: Not Handling Missing Props

// ❌ CRASHES if user prop is undefined
function Profile({ user }) {
  return <h1>{user.name}</h1>;  // TypeError if user is undefined
}

// ✅ DEFENSIVE — handle missing data
function Profile({ user }) {
  if (!user) return <p>No user data</p>;
  return <h1>{user.name}</h1>;
}

// ✅ ALSO GOOD — optional chaining
function Profile({ user }) {
  return <h1>{user?.name ?? "Unknown User"}</h1>;
}

16. Key Takeaways

  1. Props are function arguments — they're how parent components pass data to children
  2. One-way flow — props flow down the tree, never up; callbacks enable upward communication
  3. Read-only — never mutate props; create copies if you need to transform data
  4. Destructure — use { name, age } in the function signature for clean, readable code
  5. Default values — use default parameters size = "medium" (not defaultProps)
  6. Children — the special prop for content between opening/closing tags; enables composition
  7. Spread operator{...props} forwards props cleanly, but don't spread unknown objects
  8. Callbacks — pass functions as props for child-to-parent communication; name them on[Event]
  9. TypeScript — use interfaces for compile-time prop validation; far superior to PropTypes
  10. Prop drilling — passing through many layers is a code smell; use Context or composition instead

Explain-It Challenge

  1. The Water Tower: Explain React's one-way data flow to someone using the analogy of a water distribution system. How do props relate to pipes? How do callback props relate to sensors that signal the tower? Why can't water flow backwards?

  2. The Restaurant: A restaurant has a menu (parent), waiters (props), and kitchen (children). Explain how this maps to React's component communication. What happens when the kitchen needs to tell the front-of-house something? How does this relate to callback props?

  3. The Debate: Your teammate says "Just use global variables instead of props — it's simpler." Build a counter-argument explaining why explicit prop passing makes applications more maintainable, testable, and debuggable, even though it's more verbose.


Navigation: ← 2.2.a — Functional Components · Next → 2.2.c — Dynamic Rendering