Props as Inputs
So far we've been writing components which return some JSX, and that's fine, but there's an obvious problem coming up now.
What if I want to use the same LessonCard component for 20 different lessons?
I definitely don't want to create LessonCard1, LessonCard2, LessonCard3 and continue this nonsense until lesson 20 (well, I've done this many times!). The component code is mostly the same anyway. Only some values change, maybe the lesson title, how many minutes it takes, whether it's complete, etc.
This is what props are for.
<LessonCard title="Props as Inputs" minutes={14} />
<LessonCard title="Children as Composition" minutes={12} />Both lines use the same LessonCard component, but they're passing different values into it. React creates two component elements here, and when it renders them, LessonCard gets called separately for each one with the values belonging to that occurrence.
So the first one gets something containing "Props as Inputs" and 14, while the second gets "Children as Composition" and 12.
Same function code, different input each time.
That's pretty much the idea of props.
A component receives one props object
Before using destructuring everywhere, I think it's better to first see what React is actually giving our function.
Write it this way:
function LessonCard(props) {
return (
<article>
<h2>{props.title}</h2>
<p>{props.minutes} min</p>
</article>
);
}props here is one JavaScript object.
If we render:
<LessonCard title="Props as Inputs" minutes={14} />then the object is roughly:
{
title: "Props as Inputs",
minutes: 14
}React calls our component with that one object.
This is important because you might first assume React calls the function with one argument for every JSX prop:
function LessonCard(title, minutes) {
// nope
}That's not how React calls function components. title there would receive the entire props object, and minutes wouldn't receive the minutes prop.
You can log it if you want to see this yourself:
function LessonCard(props) {
console.log(props);
return <h2>{props.title}</h2>;
}You might see that log twice in development when Strict Mode is enabled. Don't start debugging your computer yet. React intentionally runs some things again in development to catch code that behaves badly when repeated.
Once you've seen the object, remove the log and move on.
A function component receives one props object. The prop names you write in JSX become properties on that object.
Who actually gives the props?
The parent does.
Suppose LessonList renders our card:
function LessonList() {
return (
<LessonCard
title="Props as Inputs"
minutes={14}
/>
);
}LessonList created that LessonCard element, so it also supplied the values.
Then LessonCard receives those values when React calls it:
function LessonCard(props) {
return <h2>{props.title}</h2>;
}This is the normal React data flow. Parent has some data, parent passes it down, child reads it and renders something from it.
The child doesn't go searching somewhere for its title. It gets the title from whoever rendered it.
And later, when we need a child to tell the parent something happened, maybe a button was clicked, the parent can pass a function as a prop. The child calls that function. We'll get into that properly later, but it's still the same idea: values are being supplied through props.
Quotes and curly braces mean different things
This one is simple, but it'll definitely cause stupid bugs if you forget it.
A JSX value written inside quotes is a string:
<LessonCard title="Props as Inputs" />So title gets:
"Props as Inputs"Curly braces mean: evaluate this as JavaScript.
<LessonCard minutes={14} />Now minutes receives the number 14.
If you write this instead:
<LessonCard minutes="14" />then you've passed the string "14".
Both might print 14 on the page and make you think everything is fine. Then you try doing arithmetic or TypeScript starts complaining and now we're finding out they're not the same value at all.
Variables also go inside braces:
const lessonMinutes = 14;
<LessonCard minutes={lessonMinutes} />And objects too:
const lesson = {
title: "Props as Inputs",
minutes: 14,
};
<LessonCard lesson={lesson} />Functions, arrays, booleans, objects, strings, numbers... props can carry normal JavaScript values. Whether you can directly put that value into JSX is a separate question.
For example, passing a function as a prop is perfectly normal. Trying to directly render that function as visible text isn't what you usually want.
Destructuring props
Once you're comfortable with the fact that React passes one object, you'll usually see components written with destructuring:
function LessonCard({ title, minutes }) {
return (
<article>
<h2>{title}</h2>
<p>{minutes} min</p>
</article>
);
}React didn't suddenly start passing two arguments here.
It's still one object.
This:
function LessonCard({ title, minutes }) {is normal JavaScript parameter destructuring. JavaScript takes the object React passed in and creates local title and minutes variables from matching properties.
So if the JSX says:
<LessonCard minutes={14} />then this works:
function LessonCard({ minutes }) {
return <p>{minutes} min</p>;
}But this doesn't:
function LessonCard({ duration }) {
return <p>{duration} min</p>;
}There is no duration property in that props object, so duration becomes undefined.
If you really want the local variable to have another name, JavaScript lets you rename while destructuring:
function LessonCard({ minutes: duration }) {
return <p>{duration} min</p>;
}The prop is still called minutes from outside:
<LessonCard minutes={14} />Inside the function we're just calling that value duration.
What if a prop isn't passed?
Nothing dramatic happens.
Take this component:
function LessonCard({ title, minutes }) {
return <p>{title}, {minutes} min</p>;
}And render it without minutes:
<LessonCard title="Props as Inputs" />Since the props object has no minutes property, minutes becomes undefined.
React doesn't print the word undefined into the page there. That expression produces no visible text, so you'll end up seeing something weird such as:
Props as Inputs, minNo crash. Just bad-looking output.
If leaving out minutes should mean some default value, we can use a normal JavaScript parameter default:
function LessonCard({ title, minutes = 10 }) {
return <p>{title}, {minutes} min</p>;
}Now both of these use 10:
<LessonCard title="Props" />
<LessonCard title="Props" minutes={undefined} />But this one doesn't:
<LessonCard title="Props" minutes={null} />null was actually supplied, so the default doesn't run.
That's plain JavaScript behavior. Parameter defaults apply when the value is undefined, not whenever the value looks empty to us.
A default value only handles undefined. It doesn't check whether the caller passed a sensible value.
Someone can still pass:
<LessonCard minutes="potato" />and JavaScript is not going to come running to save us.
TypeScript can catch many wrong values while we're writing code. Data coming from APIs, storage, user input, and other runtime sources may still need checking at runtime.
Boolean props have a shorthand
Suppose our card has a featured prop.
We can write:
<LessonCard featured={true} />But JSX lets us shorten boolean true props to:
<LessonCard featured />Same value.
If you don't write the prop:
<LessonCard />then featured is undefined.
And if you specifically want false:
<LessonCard featured={false} />then write false.
This shorthand reads nicely for names such as featured, complete, disabled, or showDetails because the name already sounds boolean.
<LessonCard complete />That reads pretty naturally: this lesson card is complete.
Don't modify props
Now we get to one rule that you should get used to early.
Read props. Don't change them.
This is bad:
function LessonCard(props) {
props.title = props.title.toUpperCase();
return <h2>{props.title}</h2>;
}The value came from the parent. The child shouldn't be reaching into its input and rewriting it while rendering.
If you want an uppercase version, make another local value:
function LessonCard({ title }) {
const displayTitle = title.toUpperCase();
return <h2>{displayTitle}</h2>;
}Much better.
title stays exactly what the parent supplied, and displayTitle is something this render calculated from it.
When the parent later renders:
<LessonCard title="Children as Composition" />React calls the child with the new props and the child calculates again.
The child doesn't update itself by changing the previous props object.
Props come from the parent. Treat them as read-only while rendering.
Objects inside props can still be mutated
There's one part here which is easy to misunderstand.
Suppose we have:
const lesson = {
title: "Props as Inputs",
complete: false,
};
<LessonCard lesson={lesson} />The lesson prop contains a reference to that object.
React isn't making some fully separate copy of the object before handing it to LessonCard.
So this is bad too:
function LessonCard({ lesson }) {
lesson.complete = true;
return <h2>{lesson.title}</h2>;
}You changed the original object.
Any other code holding that same object can now see complete: true, and you also did this mutation while the component was rendering.
Instead, just read the data:
function LessonCard({ lesson }) {
const label = lesson.complete ? "Complete" : "Not started";
return <p>{lesson.title}, {label}</p>;
}If the lesson later becomes complete, whoever owns that lesson data should update it properly and render the child with the new value.
Props being read-only doesn't only mean "don't write props.foo = something". If a prop contains an object or array, don't start modifying that object or array either.
key is a special case
You've probably seen key already in lists:
<LessonCard key={lesson.id} lesson={lesson} />You might naturally expect this:
function LessonCard(props) {
console.log(props.key);
}But key doesn't arrive as a normal prop.
React uses it to identify sibling elements while reconciling lists, so it handles key itself.
If the component also needs that ID, pass it separately:
<LessonCard
key={lesson.id}
lessonId={lesson.id}
lesson={lesson}
/>Now lessonId is a normal prop and the component can read it.
ref is another React-related prop with special behavior. In React 19, function components can receive ref as a prop, but refs still exist for a particular kind of use: getting imperative access to a DOM node or another ref-capable target.
Don't add refs to normal component data just because they're available. Most component communication should still happen through props, state, and callbacks.
Don't copy props into state for no reason
This is another one I see quite often:
function LessonCard({ title }) {
const [localTitle] = useState(title);
return <h2>{localTitle}</h2>;
}Looks harmless, right?
The problem is useState(title) only uses title when that state is initialized.
Suppose the parent first renders:
<LessonCard title="Props as Inputs" />So localTitle starts as "Props as Inputs".
Later the parent renders:
<LessonCard title="Children as Composition" />The prop changed, but localTitle doesn't automatically reinitialize. State keeps its previous value.
So now the parent is sending "Children as Composition" while the card might still display "Props as Inputs".
If you're only displaying the prop, just use the prop:
function LessonCard({ title }) {
return <h2>{title}</h2>;
}Put something into local state when the component itself needs to own changes to that value.
For example, an input field might begin with some value from props and then let the user independently edit it. That's a different requirement. But copying every prop into state by habit only gives you two values to keep track of instead of one.
Let's make the card reusable
Now we can write one LessonCard that accepts the data it needs:
export default function LessonCard({
title,
minutes,
complete = false,
}) {
const status = complete ? "Complete" : "Not started";
return (
<article>
<h2>{title}</h2>
<p>{minutes} min</p>
<p>{status}</p>
</article>
);
}And use that exact same function for different lessons:
<LessonCard
title="Props as Inputs"
minutes={14}
complete
/>Then again:
<LessonCard
title="Children as Composition"
minutes={12}
/>Nothing inside LessonCard changed between these two calls. The first element supplied one set of props, the second supplied another set, and React rendered each occurrence using those inputs.
That's really what I want you to take from props before we add more React concepts on top.
A component is a JavaScript function. React calls it with one props object. The parent decides what values go into that object when it creates the child element, and the child reads those values to calculate its JSX.
Then maybe later the parent renders again with different props, and the whole thing happens again with the newer values.
So if we follow one prop from start to finish, it goes something like this:
parent renders a component element
↓
JSX contains the prop values
↓
React calls the component with one props object
↓
component reads those values
↓
component returns UI from them
↓
parent can later render it with different valuesAnd please don't mutate the object somewhere halfway through that process.
You'll save yourself some very confusing debugging later.