React from the ground up

Props: data goes down

Passing values into a component, destructuring them in the parameter, filling a hole with children, and the rule that props are read-only.

Part 4 of 12

Beginner14 min readBack to Courses

A component with a gap in it

A component that always renders the same words is a template. Props are what turn it into a tool: <Card title="Hello" /> passes a title value in, and inside the component it arrives as one object — function Card(props) — with props.title on it.

The parameter is almost always destructured on the spot: function Card({ title, tone }). It reads like a list of what the component needs, which is the most useful documentation a component can have.

Children, and defaults

Anything written between an opening and closing tag arrives in a prop called children: <Panel>Some words</Panel> gives the component props.children, ready to be placed somewhere in its own JSX. That is how wrapper components work — they own the frame, the caller owns the contents.

A default belongs in the destructuring: function Badge({ tone = "plain" }) means the component works when the prop is missing. A missing prop you do not default is simply undefined, so a quick default beats a crash or an odd "undefined" on the page.

Read-only, both ways

Props are read-only in the strict sense: a component must not assign to them. The value belongs to whoever rendered the component, and changing it there is how a child asks for a different picture — that road leads to state, two chapters from here.

Downwards it is just JavaScript: any value can be a prop — a string, a number, a boolean, an object, an array, even a function. A boolean written plainly is true: <Badge loud /> is the same as <Badge loud={true} />.

Props are the component’s arguments

A component is a function, and props are its arguments. Everything said about arguments elsewhere applies here: the order does not matter because the names do, a missing one is undefined unless defaulted, and the component cannot change what it was given any more than a function can change the number it was called with.

The difference is who calls it. You write <Card title="Hi" /> and React does the call, at a moment you do not control and as often as the tree needs. That is why a prop is a description of the current picture rather than a message sent once.

Spreading a props object

When a wrapper passes most of its props straight through, the spread saves the typing: <Card {...props} /> hands every field on. It is convenient and it hides the contract — a reader can no longer see what Card needs — so use it for a pass-through wrapper and write the names out everywhere else.

Destructured, defaulted, filled

function Badge({ tone = 'plain' }) {
  return <span className={'badge badge--' + tone}>new</span>;
}

function Panel({ title, children }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      <div>{children}</div>
    </section>
  );
}

function App() {
  return (
    <Panel title="Today's lesson">
      <p>Props are read-only, and they travel one way: down.</p>
      <Badge />
      <Badge tone="warm" />
    </Panel>
  );
}

Props are a snapshot

Every render, React calls the component with a fresh props object. The object is not a live channel: it describes what the parent asked for at that moment. If the parent later changes a value, the child is called again with a new object; the old one never changes.

That is why mutating a prop is pointless as well as wrong. The change lives until the next render and then disappears, because the next object comes from the parent. When a child needs to remember something, it needs state, not a modified prop.

Values, not strings

A prop written in quotes is always a string, even when it looks like a number: count="3" gives the child the text "3". Braces pass the value itself: count={3} passes a number, and items={books} passes the array rather than the word books.

Booleans have a shorthand: disabled is disabled={true}, and disabled={false} is the same as leaving it out. For everything else — objects, arrays, functions, null — braces are the honest form, and they make the type visible at the call site.

Three prop mistakes worth recognising: assigning to props.title inside the child, which React overwrites on the next render; forgetting a default and printing undefined; and passing a number or a boolean in quotes, so the child compares a string with a number and quietly gets the wrong answer.

Try these next

  • Give Card a year prop as a number and render it only when it is above 2000.
  • Add a compact boolean prop and change the card padding when it is true.
  • Render a card with no note and confirm the default appears.

Cards that take what they are given

Run it, then add a `note` prop to Card that renders in small text under the title. Then render a third card with no note and give it a sensible default.

Live Preview
Console output will appear here...

Price tags with defaults

Run it, then add a fourth price with `sale` set and no `currency`, so the default has to appear. Then change the default itself and check every tag that omitted it.

Live Preview
Console output will appear here...

A component that assigns to its own props changes a value somebody else owns. React does not stop you; the next render simply overwrites it, and the bug looks like the page ignoring a click.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What may a component do with its props?