React Props

Props (short for properties) are how data flows into a React component from its parent. They work like function arguments: a parent component passes values down, and the child component reads them to decide what to render. Props are the foundation of reusable components — the same component can render differently depending on what props it receives, without you copying and pasting markup.

Understanding props deeply — how they flow, why they’re read-only, and how they differ from state — is essential before moving on to hooks, because most of React’s data model is built around this one-directional flow of props down the component tree.

Overview / How Props Work

Every function component in React accepts a single argument, conventionally called props, which is a plain JavaScript object. When you write <Greeting name="Ava" age={7} /> in JSX, React collects the attributes name and age into an object { name: "Ava", age: 7 } and passes that object as the first argument to the Greeting function. Inside the component, you read props.name and props.age (or destructure them) to render output.

Props are read-only. A component must never modify the props object or any of its properties — React treats this as a pure input, much like a function should never mutate its arguments. If a component needs to change over time in response to user interaction, it needs its own state (covered in the State lesson), not a mutated prop. This read-only rule is what makes React’s data flow predictable: data always moves one direction, from parent to child, so when something on screen looks wrong, you can trace it back up the tree to find where the value originated.

Props can be any JavaScript value: strings, numbers, booleans, arrays, objects, functions, or even other React elements (including children, discussed below). Passing a function as a prop is how children communicate back up to parents — the child calls the function it was given, and the parent’s own state update runs in response. This is often called “lifting state up” and is one of the most important patterns in React.

When a parent re-renders, React calls the child component again with a new props object. If the new props are different (by value comparison for primitives, or reference for objects/arrays/functions), the child re-renders too, producing new output. This is part of React’s render → reconcile → commit cycle: React re-runs your component function with the latest props and state, builds a new Virtual DOM tree, diffs it against the previous tree (reconciliation), and only applies the minimal set of real DOM changes (commit). Props are one of the two triggers (along with state) that cause this cycle to run.

Syntax

function ComponentName(props) {
  return <div>{props.someValue}</div>;
}

// Usage:
<ComponentName someValue="hello" anotherValue={42} />
Part Meaning
props A single object argument holding every attribute passed to the component
someValue="hello" A string literal prop (quotes, no braces)
anotherValue={42} A JavaScript expression prop (curly braces required for non-strings)
{props.children} Whatever JSX was nested between the component’s opening and closing tags
Destructuring function ComponentName({ someValue, anotherValue }) { ... } — reads props directly without the props. prefix

Examples

Example 1: Basic props

function Greeting({ name, age }) {
  return (
    <p>
      Hello, {name}! You are {age} years old.
    </p>
  );
}

function App() {
  return (
    <div>
      <Greeting name="Ava" age={7} />
      <Greeting name="Noah" age={9} />
    </div>
  );
}

Renders: a div containing two paragraphs: “Hello, Ava! You are 7 years old.” and “Hello, Noah! You are 9 years old.” The Greeting component is defined once but reused twice with different data, which is the whole point of props: one component, many outputs.

Example 2: Default values and the children prop

function Card({ title, children }) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <div className="card-body">{children}</div>
    </div>
  );
}

Card.defaultProps = {
  title: "Untitled",
};

function App() {
  return (
    <Card title="Weather">
      <p>It is sunny today.</p>
    </Card>
  );
}

Renders: a card div with an h3 reading “Weather” and a body containing the paragraph “It is sunny today.” Anything nested between <Card> and </Card> is automatically passed to Card as props.children — this is how wrapper/layout components (cards, modals, layouts) let the caller supply arbitrary content. Card.defaultProps supplies a fallback for title if the caller omits it; in modern React you can achieve the same result with a default parameter, function Card({ title = "Untitled", children }), which is the preferred style since defaultProps on function components is being phased out in favor of JS default parameters.

Example 3: Passing a function prop (child-to-parent communication)

import { useState } from "react";

function LikeButton({ liked, onToggle }) {
  return (
    <button onClick={onToggle}>
      {liked ? "Liked" : "Like"}
    </button>
  );
}

function Post() {
  const [liked, setLiked] = useState(false);

  return (
    <LikeButton
      liked={liked}
      onToggle={() => setLiked((prev) => !prev)}
    />
  );
}

Renders: a button that reads “Like”; clicking it toggles the label to “Liked” and back. Post owns the liked state and passes both the current value and an updater function down to LikeButton as props. LikeButton itself has no state — it’s a “presentational” component that just displays whatever it’s told and calls onToggle when clicked. This pattern (state lives in a parent, children get data + callbacks as props) is extremely common in React apps.

How It Works Step by Step

  • On mount: the parent’s JSX is evaluated, building a props object for each child element; React calls each child component function with that props object and renders the returned JSX.
  • On a prop change: when the parent re-renders (usually because its own state changed), it creates a new props object for the child. React calls the child function again with the new props, computes the new Virtual DOM, diffs it against the previous render, and updates only the real DOM nodes that changed.
  • On unrelated parent state changes: if the parent re-renders but passes the exact same prop values, the child still re-renders by default (React re-runs the function), but reconciliation usually finds no meaningful DOM differences, so no visible change occurs. (You can prevent the extra function call itself with memo, covered in a later lesson.)
  • On unmount: if the parent stops rendering the child (e.g., a conditional becomes false), React removes it from the tree and discards its props along with any state and effects it had.

Common Mistakes

Mistake 1: Mutating props directly.

function UserCard(props) {
  props.user.name = props.user.name.toUpperCase(); // mutates the parent's object!
  return <p>{props.user.name}</p>;
}

This mutates an object the parent still owns and may rely on elsewhere, causing subtle bugs that show up far from the code that caused them. Instead, derive a new value without touching the original:

function UserCard({ user }) {
  return <p>{user.name.toUpperCase()}</p>;
}

Mistake 2: Forgetting a key when rendering a list of components with props.

function List({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li>{item.label}</li>
      ))}
    </ul>
  );
}

Without a key, React can’t efficiently track which list item is which between renders, and you’ll see a console warning plus potential state/ordering bugs if the list changes. Add a stable, unique key prop:

function List({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.label}</li>
      ))}
    </ul>
  );
}

Mistake 3: Excessive prop drilling. Passing the same prop through four or five intermediate components that don’t use it themselves — just to get it to a deeply nested child — makes components hard to reuse and refactor. When this happens repeatedly for the same piece of data, it’s usually a sign you should reach for useContext (covered in its own lesson) instead of threading props through every layer.

Best Practices

  • Destructure props in the function signature (function Card({ title, children })) instead of writing props.title everywhere — it’s more readable and documents exactly what a component expects.
  • Never mutate props or any object/array received via props; treat everything coming in as read-only.
  • Use JS default parameters for optional props ({ size = "medium" }) rather than relying only on defaultProps.
  • Keep prop names descriptive and consistent across similar components (e.g., always onClick, not sometimes onPress).
  • Pass only the data and callbacks a component actually needs, rather than a giant catch-all object — this keeps components easier to test and reuse.
  • Use children for components that wrap arbitrary content (cards, modals, layout containers) instead of a custom content prop.
  • Reach for useContext when the same prop is drilled through many unrelated layers.

Practice Exercises

  • Create a Button component that accepts label and onClick props and renders a <button>. Render three buttons in an App component, each with a different label and its own click handler that logs a different message.
  • Create a Badge component that accepts a status prop (one of "active", "inactive", "pending") and renders different text and a different className depending on the value. Give status a default of "pending".
  • Create a Panel component that renders a title prop inside an <h3> and its children inside a <div>. Use it to wrap two different pieces of content in your App component.

Summary

  • Props are a read-only object passed from a parent component into a child, similar to function arguments.
  • Any JS value can be a prop: strings, numbers, booleans, arrays, objects, or functions.
  • Functions passed as props let children communicate back up to parents (e.g., onClick, onToggle).
  • The special children prop holds whatever JSX is nested between a component’s opening and closing tags.
  • Never mutate props — always derive new values instead.
  • Excessive prop drilling across many layers is a sign to use useContext instead.