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.