Lesson 5 of 20

Props & Data Flow

Props Are a Component's Parameters

A component that always renders the same thing is not much use. Props, short for properties, are how a parent hands data down to a child, and they are nothing more exotic than function arguments. Every attribute you write on a component tag becomes a key in one object, and React passes that object as the component function's first argument.

You could accept the whole object and write props.name everywhere, and you will see code that does. Most React code destructures instead, pulling the fields out in the parameter list. It is the same object either way — destructuring just saves you repeating props. and makes the component's inputs readable at a glance, which is genuinely useful when you open a file you have not seen before.

Because props are ordinary arguments, one component definition can produce any number of different-looking results. The OrderRow below is written once and used for every row in an order history table; each use passes different values and gets different output. That is the whole reuse story.

Example
// Attributes on the tag become one object.
// <OrderRow id="OD-4417" item="Wireless mouse" total={899} delivered />
// arrives as { id: 'OD-4417', item: 'Wireless mouse', total: 899, delivered: true }

// Style 1 — take the whole object
function OrderRow(props) {
  return <tr><td>{props.id}</td><td>{props.item}</td></tr>;
}

// Style 2 — destructure in the parameter list (what most code does)
function OrderRow({ id, item, total, delivered }) {
  return (
    <tr>
      <td>{id}</td>
      <td>{item}</td>
      <td>₹{total}</td>
      <td>{delivered ? 'Delivered' : 'In transit'}</td>
    </tr>
  );
}

function OrderHistory() {
  return (
    <table>
      <tbody>
        <OrderRow id="OD-4417" item="Wireless mouse" total={899} delivered />
        <OrderRow id="OD-4418" item="USB-C cable" total={249} />
      </tbody>
    </table>
  );
}
Notes
  • Name props for what the data is, not for how it will be displayed. isAdmin survives a redesign; showRedBadge does not.

Passing Anything That Is Not a String

Quotes in JSX always mean a literal string. The moment you want a number, a boolean, an array, an object or a function, you switch to curly braces. This distinction causes a specific and very common bug: quantity="5" passes the two-character string "5", so quantity + 1 inside the child produces "51" rather than 6. Write quantity={5}.

Booleans have a shorthand worth knowing. Writing a prop name with no value at all passes true, so <Button disabled /> and <Button disabled={true} /> mean the same thing. To pass false you must be explicit, or leave the prop off entirely.

Objects need two sets of braces — the outer pair is the JSX escape into JavaScript, the inner pair is the object literal — which is the same doubling you already met with the style attribute. And functions are passed exactly like any other value, which is the foundation of the next section.

Example
<ProductCard
  name="Cotton kurta"          // string  -> quotes
  price={1299}                  // number  -> braces
  inStock                       // boolean true (shorthand)
  featured={false}              // boolean false must be explicit
  sizes={['S', 'M', 'L']}       // array
  seller={{ name: 'Vasant Textiles', rating: 4.3 }}   // object: two braces
  onAddToCart={handleAddToCart} // function
/>

// Inside the child
function ProductCard({ name, price, inStock, sizes, seller, onAddToCart }) {
  return (
    <div>
      <h3>{name}</h3>
      <p>₹{price} — {sizes.join(', ')}</p>
      <p>Sold by {seller.name} ({seller.rating}★)</p>
      <button disabled={!inStock} onClick={onAddToCart}>Add to cart</button>
    </div>
  );
}
  • text="hello" — a string
  • count={5} — a number; count="5" is a string and will bite you in arithmetic
  • isOpen — shorthand for isOpen={true}
  • items={[1, 2, 3]} — an array
  • user={{ name: 'Riya' }} — an object, hence the double braces
  • onSave={handleSave} — a function reference, with no parentheses
Notes
  • Passing a new object or array literal inline, as in seller={{ ... }}, creates a fresh value on every render of the parent. That is harmless most of the time, but it matters for performance optimisation and for effect dependencies — two later lessons come back to it.

Props Are Read-Only

A component must never modify its own props. Not props.count++, not user.name = 'x', not items.push(...). The data belongs to whoever passed it down, and the child is a borrower.

The reason is not politeness. React re-renders a child because its parent re-rendered with new props; if the child edits the props object, the change does not tell React anything, so nothing re-renders and the screen keeps showing the old value. Worse, if the prop is an object or array, the child has just edited data the parent still holds, so the parent's own copy is now wrong. You end up with a value that has changed but is not displayed, and a display that updates only when something unrelated happens to trigger a render.

The right instinct is that props flow strictly one way: down. When a child needs different data, it does not edit what it was given — it asks the owner to change it. That is what callback props are for.

Example
// WRONG — the child edits data it does not own
function CartItem({ item }) {
  function increase() {
    item.qty = item.qty + 1;   // no re-render, and the parent's data is now corrupted
  }
  return <button onClick={increase}>{item.qty}</button>;
}

// RIGHT — the child reports what happened; the owner decides what to do
function CartItem({ item, onIncrease }) {
  return <button onClick={() => onIncrease(item.id)}>{item.qty}</button>;
}
Notes
  • The same rule applies to arrays reached through props. props.items.sort() and props.items.push() both mutate the parent's array. Copy first with [...props.items] if you need a different order or an extra element for display purposes.

Sending Data Back Up with Callback Props

Since functions are values, a parent can pass one down as a prop, and the child can call it when something happens. Data still only flows downward — what travels upward is a notification, plus whatever arguments the child chooses to send. This one pattern replaces every situation where you might have wanted a child to reach up and change its parent.

The convention is to name the prop starting with on and the function that implements it starting with handle. So a parent defines handleDelete and passes it as onDelete. It reads well at both ends and instantly tells a reader which side owns the logic.

The mistake here is passing a call instead of a reference. onClick={onDelete(item.id)} runs onDelete immediately during render and hands React whatever it returned, usually undefined. Symptoms: the delete happens the moment the list appears, or nothing happens on click, or you get an infinite loop of updates. When you need to pass an argument, wrap the call in an arrow function — onClick={() => onDelete(item.id)} — which creates a function that will call it later.

Example
// The parent owns the list and the logic that changes it.
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 't1', text: 'Submit assignment', done: false },
    { id: 't2', text: 'Book train ticket', done: false }
  ]);

  function handleDelete(id) {
    setTodos(todos.filter(t => t.id !== id));
  }

  function handleToggle(id) {
    setTodos(todos.map(t => (t.id === id ? { ...t, done: !t.done } : t)));
  }

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

// The child owns nothing. It renders, and it reports.
function TodoItem({ todo, onDelete, onToggle }) {
  return (
    <li>
      <input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>

      {/* WRONG: runs during render, deletes immediately */}
      {/* <button onClick={onDelete(todo.id)}>Delete</button> */}
    </li>
  );
}
  • Name the prop onSomething and the parent's function handleSomething
  • Pass the reference: onClick={handleDelete}
  • Need an argument? Wrap it: onClick={() => handleDelete(id)}
  • Never onClick={handleDelete(id)} — that calls it while rendering
  • The child decides when something happened; the parent decides what it means
Notes
  • This is why the same TodoItem can be reused in a completed-tasks screen that passes a different onDelete, or in a read-only report that passes none at all. The child has no opinion about what deleting does.

children, Defaults and Spreading

Three smaller pieces of prop mechanics show up in nearly every codebase. The first is children: whatever sits between a component's opening and closing tags arrives as a prop of that name. It is what makes wrapper components — cards, modals, page layouts — possible, and it is the reason <Card>anything at all</Card> works without Card knowing anything about the contents.

The second is default values. Because props are function parameters, ordinary JavaScript defaults apply: function Button({ variant = 'primary' }). There is one subtlety worth internalising — a default fills in only when the value is undefined, which includes leaving the prop off entirely. It does not fill in for null, 0 or an empty string. Pass count={0} to a parameter defaulted to 10 and you get 0, which is usually what you want but occasionally surprises people debugging a missing default.

The third is spreading. <Input {...props} /> forwards every prop it received to the element below. It is genuinely useful for thin wrappers around HTML elements, where you want your component to accept placeholder, maxLength, autoFocus and everything else without listing them. Used anywhere else it is a liability: a reader can no longer tell what a component actually receives, and a typo in a prop name will pass through silently instead of being noticed.

Example
// children — the contents arrive as a prop
function Card({ title, children }) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <div className="card-body">{children}</div>
    </div>
  );
}

<Card title="Fee summary">
  <p>Tuition: ₹42,000</p>
  <button>Download receipt</button>
</Card>

// Defaults — used only when the prop is undefined
function Button({ label = 'Submit', variant = 'primary', ...rest }) {
  return <button className={`btn btn-${variant}`} {...rest}>{label}</button>;
}

// Spreading — sensible here, because Button is a thin wrapper over <button>
<Button label="Save" type="submit" disabled={saving} aria-label="Save form" />
Notes
  • Notice ...rest in the parameter list: it collects every prop you did not name, so the wrapper can forward type, disabled and accessibility attributes to the real element without knowing them in advance.

When Passing Props Starts to Hurt

One-way data flow has a cost, and you will meet it. Suppose the logged-in user's name is needed by a small avatar deep inside a navbar, inside a header, inside a page layout. The value has to be threaded through three components that have no interest in it whatsoever, each one accepting a prop only to hand it straight on. This is called prop drilling, and past a couple of levels it makes components tedious to change.

Before reaching for a bigger tool, try the cheap fixes. Often the value is being drilled because the component tree is shaped awkwardly, and passing the finished JSX down as children removes the problem entirely — the layout does not need the user's name if it is handed a ready-made avatar element. Often the state is simply held too high and can be moved closer to where it is used. Only when a genuinely global value is needed in many unrelated places does Context earn its complexity, and there is a full lesson on it later.

The other thing worth noticing is that drilling is a symptom, not a disease. Two levels of passing props is completely normal and needs no fix at all. Reach for a solution when the passing is repetitive and the intermediate components clearly do not care.

  • One or two levels of prop passing — normal, leave it alone
  • Many levels of pass-through props — consider restructuring before adding tools
  • Composition first: pass ready-made JSX through children instead of raw data
  • Move state down if only one branch of the tree uses it
  • Genuinely global values such as theme, language or the signed-in user — Context
  • Large amounts of shared, frequently updated state — a dedicated state library
Notes
  • In a plain JavaScript project you might reach for a global variable here. Resist it in React: a value changed outside React's knowledge will not re-render anything, so the screen and the data go out of step — the exact problem React exists to prevent.
Ask AI