← React for Beginners

Props: passing data down

Lesson 04 of 12 · 15 min

The last lesson ended with a complaint. SiteHeader has its words baked into it:

function SiteHeader() {
  return (
    <header>
      <h1>My first React app</h1>
      <p>Learning the basics.</p>
    </header>
  );
}

Want a second header saying something else and there is nothing to do but copy the function and edit the copy. Which is the same problem you would have with any function whose values were hard-coded, and it has the same fix: let the caller pass them in.

Giving a component an argument

React calls your component with one argument: an object holding everything the caller wrote as an attribute. It is called props, short for properties.

function SiteHeader(props) {
  return (
    <header>
      <h1>{props.title}</h1>
      <p>{props.tagline}</p>
    </header>
  );
}

function App() {
  return (
    <>
      <SiteHeader title="My first React app" tagline="Learning the basics." />
      <SiteHeader title="Second header" tagline="Different words, same component." />
    </>
  );
}

Two headers, one function. The attributes you write on <SiteHeader /> become keys on the object React hands it, and {props.title} is the brace syntax from the last lesson doing exactly what it did before: evaluate an expression, put the result here.

That is the whole mechanism. Everything below is convenience or consequence.

Destructuring, which is what you will actually write

Repeating props. on every line gets old, so nearly all React code destructures the object in the parameter list:

function SiteHeader({ title, tagline }) {
  return (
    <header>
      <h1>{title}</h1>
      <p>{tagline}</p>
    </header>
  );
}

This is plain JavaScript, not a React feature. { title, tagline } in a parameter position means “take the object you were passed, pull those two keys out, and name them as local variables”. The two versions above are identical in behaviour; the second is what you will see everywhere, and it has the useful side effect of listing a component’s inputs on its first line.

Passing things that are not strings

The rule is the one from lesson 3: quotes for a literal string, braces for any other expression.

<Article
  title="Props"
  wordCount={1500}
  published={true}
  tags={["react", "beginners"]}
  author={{ name: "Gjorge", url: "https://karakabakov.com" }}
/>

wordCount arrives as the number 1500, not the string "1500". tags arrives as a real array you can call .map() on. author is that double brace again: outer braces for the expression, inner braces for the object literal.

Two shorthands worth recognising, because you will read them before you write them:

<Article published />           {/* same as published={true} */}
<Article {...articleData} />    {/* spread every key of the object as a prop */}

The spread is handy and easy to overuse. Written out, props are a list of what a component receives; spread, they are a mystery that depends on whatever the object happened to hold.

Defaults

A component often has a sensible fallback for a prop nobody passed. Because props are a plain object and destructuring is plain JavaScript, you get this for free:

function SiteHeader({ title, tagline = "A React practice project" }) {
  return (
    <header>
      <h1>{title}</h1>
      <p>{tagline}</p>
    </header>
  );
}

Leave tagline off and the default is used. Pass it and yours wins.

Note that the default only fires when the prop is undefined, which includes not passing it at all. Pass tagline={null} and you get null, not the default, because null is a value somebody chose.

If you find older code using a defaultProps property to do this, that is the pre-hooks way, and React 19 dropped it for function components. Worth knowing how it fails: it does not. Set Greeting.defaultProps = { name: "friend" } today and there is no error and no console warning - the prop simply arrives as undefined and your fallback never happens. A default parameter is the answer now.

children: the slot

One prop is special enough to have its own syntax. Whatever you put between a component’s opening and closing tags arrives as a prop called children:

function Panel({ title, children }) {
  return (
    <section>
      <h2>{title}</h2>
      {children}
    </section>
  );
}

function App() {
  return (
    <Panel title="Today">
      <p>Anything at all.</p>
      <p>As much as you like.</p>
    </Panel>
  );
}

Panel has no idea what is inside it, which is the point. It owns the frame - a section, a heading, whatever border you give it - and the caller owns the contents. That split is how you write a component that is genuinely reusable rather than one that is reusable in the three cases you thought of.

A component that takes text can render a string. One that takes children can render a paragraph, a form, a table, or three more components.

Props go one way, and they are read-only

Data flows down. A parent passes to a child. A child cannot reach up and hand something back, and cannot see what its siblings were given. If you know what a component received, you know everything about what it will render - you do not have to go looking for who else might have got at it. That constraint is most of why a React codebase stays readable as it grows.

A component must not change what it was given. Props belong to the caller, and React’s protection here covers exactly half of the problem.

Try to add or change a key on the props object itself and you get stopped:

function Mutator(props) {
  props.injected = "yes";
  return null;
}
TypeError: Cannot add property injected, object is not extensible

React seals that object while you are developing, so this one is hard to get wrong. Only while you are developing, mind: the built version seals nothing, and the same line succeeds there without a word. Which is unfortunate on both counts, because it teaches you that React is looking after this, and it is not. Anything inside props is an ordinary object or array, and nothing is guarding it in either build:

function TagList({ tags }) {
  tags.sort();                       // NO. This reorders the caller's array.
  return <p>{tags.join(", ")}</p>;
}

sort sorts in place, so that line reaches back up and rearranges data belonging to whoever rendered <TagList />. Nothing throws and nothing is logged. The parent’s array is simply in a different order from then on, and the symptom turns up somewhere else entirely.

So the mistake React catches is the one you were never going to make, and the one that quietly corrupts your data is the one it lets through.

Copy first:

function TagList({ tags }) {
  const sorted = [...tags].sort();   // a copy, sorted
  return <p>{sorted.join(", ")}</p>;
}

Or use toSorted(), which returns a new array and leaves the original alone:

const sorted = tags.toSorted();

The same applies to push, splice, reverse and assigning to a property of an object prop. If a method changes the thing it was called on, it is the wrong method here. This will come back in lesson 7, where the rule turns out to apply to state as well, for related reasons.

Where this leaves you

Put together, App now looks like an outline with its content filled in from one place:

function SiteHeader({ title, tagline = "A React practice project" }) {
  return (
    <header>
      <h1>{title}</h1>
      <p>{tagline}</p>
    </header>
  );
}

function Panel({ title, children }) {
  return (
    <section>
      <h2>{title}</h2>
      {children}
    </section>
  );
}

function Footer() {
  return <footer>Built in {new Date().getFullYear()}.</footer>;
}

function App() {
  return (
    <>
      <SiteHeader title="My first React app" />
      <main>
        <Panel title="Today">
          <p>The interesting part goes here.</p>
        </Panel>
      </main>
      <Footer />
    </>
  );
}

export default App;

You can render one Panel by writing one <Panel />. What do you write when the panels come from an array of twenty things fetched from somewhere, and you do not know how many there will be?

Recap

Props are the arguments a component is called with, delivered as a single object and almost always destructured on the parameter line. Quotes pass a string, braces pass any other expression, a default parameter covers the case where nobody passed anything, and whatever sits between a component’s tags arrives as children. Data moves in one direction only, and a component reads its props without ever modifying them.

Next: turning an array into markup, and the one extra thing React asks for when you do.