Get eBook BundleVolume I index
Components Before Abstractions

Props as Inputs

Ishtmeet Singh @ishtms/July 20, 2026/11 min read
#react#props#components#jsx#data-flow

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.

jsx
<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:

jsx
function LessonCard(props) {
  return (
    <article>
      <h2>{props.title}</h2>
      <p>{props.minutes} min</p>
    </article>
  );
}

props here is one JavaScript object.

If we render:

jsx
<LessonCard title="Props as Inputs" minutes={14} />

then the object is roughly:

js
{
  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:

jsx
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:

jsx
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.

Who actually gives the props?

The parent does.

Suppose LessonList renders our card:

jsx
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:

jsx
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:

jsx
<LessonCard title="Props as Inputs" />

So title gets:

js
"Props as Inputs"

Curly braces mean: evaluate this as JavaScript.

jsx
<LessonCard minutes={14} />

Now minutes receives the number 14.

If you write this instead:

jsx
<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:

jsx
const lessonMinutes = 14;

<LessonCard minutes={lessonMinutes} />

And objects too:

jsx
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:

jsx
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:

jsx
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:

jsx
<LessonCard minutes={14} />

then this works:

jsx
function LessonCard({ minutes }) {
  return <p>{minutes} min</p>;
}

But this doesn't:

jsx
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:

jsx
function LessonCard({ minutes: duration }) {
  return <p>{duration} min</p>;
}

The prop is still called minutes from outside:

jsx
<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:

jsx
function LessonCard({ title, minutes }) {
  return <p>{title}, {minutes} min</p>;
}

And render it without minutes:

jsx
<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:

text
Props as Inputs,  min

No crash. Just bad-looking output.

If leaving out minutes should mean some default value, we can use a normal JavaScript parameter default:

jsx
function LessonCard({ title, minutes = 10 }) {
  return <p>{title}, {minutes} min</p>;
}

Now both of these use 10:

jsx
<LessonCard title="Props" />
<LessonCard title="Props" minutes={undefined} />

But this one doesn't:

jsx
<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.

Someone can still pass:

jsx
<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:

jsx
<LessonCard featured={true} />

But JSX lets us shorten boolean true props to:

jsx
<LessonCard featured />

Same value.

If you don't write the prop:

jsx
<LessonCard />

then featured is undefined.

And if you specifically want false:

jsx
<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.

jsx
<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:

jsx
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:

jsx
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:

jsx
<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.

Objects inside props can still be mutated

There's one part here which is easy to misunderstand.

Suppose we have:

jsx
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:

jsx
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:

jsx
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:

jsx
<LessonCard key={lesson.id} lesson={lesson} />

You might naturally expect this:

jsx
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:

jsx
<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:

jsx
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:

jsx
<LessonCard title="Props as Inputs" />

So localTitle starts as "Props as Inputs".

Later the parent renders:

jsx
<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:

jsx
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:

jsx
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:

jsx
<LessonCard
  title="Props as Inputs"
  minutes={14}
  complete
/>

Then again:

jsx
<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:

text
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 values

And please don't mutate the object somewhere halfway through that process.

You'll save yourself some very confusing debugging later.