Blog

Part 3 · React Props: Passing Data to a Component

Props are the values a parent gives to a child component. Learn how to pass them, type them, give them defaults, use children, and why a component must never change its props.

In Part 2 we wrote a Greeting component and used it twice: once for Ana, once for Ben. Each time, we gave it a different name. Those names were props.

This part is all about props. We’ll see how a parent passes them, how a child reads them, and how TypeScript checks them. We’ll also meet children, the special prop that Part 2 promised to explain. And we’ll find out why a component must never change its own props.

Try this first

Read this code. Don’t press Run yet.

function ProfileCard(props: { name: string; age: number }) {
  return (
    <div>
      <h2>{props.name}</h2>
      <p>Age: {props.age}. Next year: {props.age + 1}</p>
    </div>
  )
}

export default function App() {
  return (
    <div>
      <ProfileCard name="Ana" age={12} />
      <ProfileCard name="Ben" age={9} />
    </div>
  )
}

There is one ProfileCard function, and App uses it twice. Make a guess. What will the second card show for “Next year”?

Now press Run.

The first card says “Ana”, 12 and 13. The second card says “Ben”, 9 and 10. The same function drew two different cards. The only thing that changed was what App wrote in each tag: name="Ana" age={12} the first time, and name="Ben" age={9} the second time.

Press Edit. Add a third card with your own name and age, and run it again.

What props are

A prop is a value that a parent component gives to a child component. Here App is the parent, because it uses <ProfileCard /> inside itself. ProfileCard is the child.

You write props like HTML attributes, inside the tag: name="Ana" and age={12}. React puts them all into one object, called the props object. Then React calls your component and passes that object to it. For the first card, the object is { name: "Ana", age: 12 }.

So props work like the arguments of a function. React’s docs say it this way: “props are the only argument to your component!”

Inside ProfileCard, the props object is called props. That’s why the code reads props.name and props.age. You can choose another name for it, but props is the usual one.

Let’s follow one card from the tag to the page. This time the component is a little shorter.

function ProfileCard({ name, age }: { name: string; age: number }) {
  return <p>{name} is {age}</p>
}

export default function App() {
  return <ProfileCard name="Ana" age={12} />
}
1. The parent writes a tag and gives it two props. 2. The props go into one object, inside the element. 3. React calls your function with that props object. 4. Destructuring takes each value out of the object. 5. Your function uses them in the elements it returns. 6. React puts a real <p> on the page. <ProfileCard name="Ana" age={12} /> { type: ProfileCard, props: { name: "Ana", age: 12 } } ProfileCard({ name: "Ana", age: 12 }) name is "Ana" age is 12 { type: "p", props: { children: ["Ana", " is ", 12] } } Ana is 12the browser page now has <p>Ana is 12</p>

How props travel from the parent to the page. Press play, or step through it.

Here are the same steps in words.

  1. The parent writes <ProfileCard name="Ana" age={12} />.
  2. That makes an element, as you saw in Part 2. Its props is one object: { name: "Ana", age: 12 }.
  3. When React renders the element, it calls ProfileCard and passes in that object.
  4. { name, age } takes the two values out of the object. Now name is "Ana" and age is 12. The next section explains how.
  5. ProfileCard returns a <p> element. Its children are "Ana", " is " and 12.
  6. React puts a real <p> on the page: “Ana is 12”.

We checked these steps with React 19.3, with Strict Mode off. React called ProfileCard once. The object it passed in was the very same object as the element’s props.

Taking props out of the object

In Part 2 we wrote function Greeting({ name }). We only said the curly braces take name out of the object. Now let’s see how.

Writing props.name and props.age again and again gets long. JavaScript has a shorter way to take values out of an object. It is called destructuring. You list the names you want inside { }, and JavaScript makes a variable for each one.

These two components do the same thing:

function CardA(props: { name: string; age: number }) {
  const name = props.name
  const age = props.age
  return <p>{name} is {age}</p>
}

function CardB({ name, age }: { name: string; age: number }) {
  return <p>{name} is {age}</p>
}

export default function App() {
  return (
    <div>
      <CardA name="Ana" age={12} />
      <CardB name="Ana" age={12} />
    </div>
  )
}

In CardB, the { name, age } sits where the parameter goes. A parameter is the name a function gives to the value it is called with. So CardB gets the same props object, and takes name and age out of it at once.

React’s docs say you usually don’t need the whole props object. So most of the time, you take out the props you need, as CardB does. Don’t forget the curly braces, though. Without them, you get the whole object, not the name. You’ll see what that does under Common mistakes.

Passing different kinds of values

A prop can hold any JavaScript value. How you write it depends on the kind of value.

  • Text goes in quotes: name="Ana".
  • Everything else goes in curly braces, as you saw in Part 1. That means numbers, true and false, arrays, objects and functions.
<ProfileCard
  name="Ana"
  age={12}
  isOnline={true}
  hobbies={['chess', 'music']}
  pet={{ kind: 'cat', name: 'Momo' }}
/>

Look at pet={{ ... }}. As with style in Part 1, the outer braces mean “JavaScript here”. The inner braces are the object.

We passed one of each kind to a component and asked JavaScript’s typeof what arrived. typeof tells you the kind of a value.

You write The child gets typeof says
text="hello" "hello" string
num={12} 12 number
numText="12" "12" string
yes={true} true boolean
disabled true boolean
list={['a', 'b']} ['a', 'b'] object (an array)
person={{ name: 'Ana' }} { name: 'Ana' } object
onPick={() => 1} a function function

Two rows need a closer look.

numText="12" arrived as text, not as a number. Quotes always mean text. That causes a real bug, which you’ll meet under Common mistakes.

disabled has no value at all, and it arrived as true. This is a short way to write disabled={true}. It’s useful for props that are on or off:

function Button({ label, disabled = false }: { label: string; disabled?: boolean }) {
  return <button disabled={disabled}>{label}</button>
}

export default function App() {
  return (
    <div>
      <Button label="Send" />
      <Button label="Wait" disabled />
    </div>
  )
}

Run it. You can click “Send”, but not “Wait”. The next section explains the ? and the = false.

A function passed as a prop lets a child tell its parent that something happened, like a click. That needs events and state, so it comes later, in Parts 6 and 9.

Prop types in TypeScript

TypeScript wants to know what props a component takes, and what kind each one is. You have already seen the short way, written right after the parameter:

function Greeting({ name }: { name: string }) {
  return <p>Hello, {name}!</p>
}

With more props, this gets hard to read. So you can give the props their own type first, with a name. A type is a description of what a value must look like.

type ProfileCardProps = {
  name: string
  age: number
  city?: string
}

function ProfileCard({ name, age, city = 'Lisbon' }: ProfileCardProps) {
  return <p>{name} is {age} and lives in {city}.</p>
}

export default function App() {
  return (
    <div>
      <ProfileCard name="Ana" age={12} />
      <ProfileCard name="Ben" age={9} city="Pune" />
    </div>
  )
}

Three new things are in this example.

  • type ProfileCardProps = { ... } names the shape of the props. React’s own TypeScript page ends such names with Props too, like MyButtonProps.
  • city?: string has a question mark. It means city is optional: the parent may leave it out. name and age have no question mark, so they are required. Leave out a required prop, and TypeScript stops you: Property 'name' is missing.
  • city = 'Lisbon' gives city a default value. If the parent doesn’t pass city, the component uses 'Lisbon'.

When a default is used

A default is used in only two cases. The prop is missing, or its value is undefined. We tested five ways to call a component that has size = 100:

function Avatar({ size = 100 }: { size?: number | null }) {
  return <p>size: [{size}]</p>
}

export default function App() {
  return (
    <div>
      <Avatar />
      <Avatar size={undefined} />
      <Avatar size={null} />
      <Avatar size={0} />
      <Avatar size={50} />
    </div>
  )
}

The first two used the default, 100. null did not, and it shows nothing, as Part 1’s table said. 0 did not either. 0 is a real value, so the component shows it.

An optional prop with no default is undefined when it’s missing. On the page, undefined shows nothing. So <p>Hello, {name}!</p> with no name shows “Hello, !”. We checked.

children: what you put between the tags

So far, every tag closed itself: <ProfileCard ... />. But you can also put things between a component’s opening and closing tags. You do the same with <div>:

import type { ReactNode } from 'react'

function Card({ title, children }: { title: string; children: ReactNode }) {
  return (
    <section>
      <h2>{title}</h2>
      <div>{children}</div>
    </section>
  )
}

export default function App() {
  return (
    <div>
      <Card title="Welcome">
        <p>Anything can go here.</p>
      </Card>
      <Card title="Shopping list">
        <ul>
          <li>Bread</li>
          <li>Milk</li>
        </ul>
      </Card>
    </div>
  )
}

Run it. Both cards have a heading and a box, but different things inside.

Whatever you put between <Card> and </Card>, React passes to Card as a prop named children. You don’t write children= anywhere. React fills it in for you. Card then decides where it goes, by writing {children} in its JSX.

Card doesn’t need to know what goes inside it. It only draws the frame. React’s docs describe a component like this as having a “hole” that the parent fills in. Building bigger parts of a page out of smaller components like this is called composition. Part 19 shows how it can also save work when the page updates.

What children holds

children is not always the same kind of value. It depends on what you put between the tags. We checked each case with React 19.3:

Between the tags children is
nothing: <Box /> not there (undefined)
Hello the text "Hello"
<b>Hi</b> one element
<p>One</p><p>Two</p> an array of 2 elements
Hello, {name}! an array of 3: "Hello, ", "Ana", "!"
{42} the number 42

That’s why the type is ReactNode. You met it in Part 1. It means “anything React can show”. That includes an element, text, a number and an array of these. React’s types say it also includes true, false, null and undefined. You bring it in with import type { ReactNode } from 'react'.

Should children be required? With children: ReactNode, writing <Card title="Hi" /> with nothing inside is a TypeScript error: Property 'children' is missing. If a card may be empty, write children?: ReactNode instead.

Say you forget to list children in the type at all. Then TypeScript stops you when you put something between the tags: Property 'children' does not exist.

Props are read-only

A component may read its props. It must never change them. React’s docs call props “read-only”.

Let’s see what happens if you try:

function Greeting(props: { name: string }) {
  props.name = props.name.toUpperCase()
  return <p>Hello, {props.name}!</p>
}

export default function App() {
  return <Greeting name="Ana" />
}

toUpperCase() gives back the text in capital letters. So this line tries to put “ANA” into props.name.

TypeScript doesn’t stop this line. But press Run, and React stops with an error:

Cannot assign to read only property 'name' of object '#<Object>'

While you develop, React freezes the props object. A frozen object can’t be changed. In modern code, which is all code in this series, changing it is an error. (We ran it with the JavaScript engine that the Chrome browser uses. Other browsers may word the message differently.)

The freezing happens only in development. We ran the same component with React’s production build. There the props object was not frozen. The change went through, and the page showed “Hello, ANA!” with no error. So the rule still matters when nothing stops you.

What you can do is make a new variable:

function Greeting({ name }: { name: string }) {
  const loud = name.toUpperCase()
  return <p>Hello, {loud}!</p>
}

export default function App() {
  return <Greeting name="Ana" />
}

loud belongs to this run of Greeting. The props stay as they were.

Why the rule exists

Part 2 said a component should give the same result every time it runs with the same inputs. Props are the main input. If a component changes its props, the next run starts from different inputs. Then the result changes too.

React only freezes the props object itself, not the objects inside it. We checked: a player object passed as a prop was not frozen. So this mistake gets no error at all:

type Player = { name: string; points: number }

const ana: Player = { name: 'Ana', points: 10 }

function Score({ player }: { player: Player }) {
  player.points = player.points + 5
  return <p>{player.name}: {player.points} points</p>
}

export default function App() {
  return <Score player={ana} />
}

Guess first. Ana starts with 10 points, and Score adds 5. What does the page show?

Press Run. It shows “Ana: 20 points”, not 15.

Strict Mode is the reason. As Part 2 explained, Strict Mode runs each component twice while you develop. Both runs got the same player object, and each run added 5. With Strict Mode off, we measured 15. And in both cases, the ana object at the top of the file was changed too.

That is the bug Strict Mode is built to show you. A component that changes its inputs gives a different answer each time it runs. The fix is the same as before: make a new value, const total = player.points + 5, and show total.

An everyday example

Props are like a note a teacher hands to a student: “Read page 12”. The student reads the note and does the work. The student doesn’t cross out “12” and write “40”. If the teacher wants a different page, the teacher writes a new note.

The exact version

The note leaves out two things. First, a child can’t change its props, but its parent can pass new ones. When the component that writes <Score … /> renders again, it makes a new element with a new props object. Each one holds the values from one moment. Second, “frozen” is only a development check. The rule is yours to keep in production too, where nothing checks it.

Data flows one way

Props go in one direction: from the parent down to the child. React’s docs call this one-way data flow. The data flows “down from the top-level component to the ones at the bottom”.

So a child can’t reach up and change its parent’s data. ProfileCard can’t change the name that App holds. It can only show what it was given.

That sounds limiting. How does a click in a child change anything? Three later parts answer it.

  • Part 4 shows state: data a component remembers and can change.
  • Part 6 shows events, like clicks and typing.
  • Part 9 puts them together. The parent keeps the data, and passes the child a function as a prop. The child calls that function, and the parent changes its own data.

Sometimes a value must travel down through many layers of components. Each middle component passes it on without using it. This has a name: prop drilling. Part 23 shows React’s tool for it, context.

New props, new render

Props can still change over time. When a parent renders again, it can pass its child components new values. The child then runs again with the new props.

This example uses useState, which Part 4 explains. All that matters here is that clicking the button makes App run again, with a bigger number.

import { useState } from 'react'

function Score({ points }: { points: number }) {
  console.log('Score runs with', points)
  return <p>Points: {points}</p>
}

export default function App() {
  const [points, setPoints] = useState(0)
  return (
    <div>
      <button onClick={() => setPoints(points + 10)}>Add 10</button>
      <Score points={points} />
    </div>
  )
}
Score runs with 0
Score runs with 0
Score runs with 10
Score runs with 10
Score runs with 20
Score runs with 20

Press Run, then click “Add 10” twice. Each line shows up twice. That is Strict Mode again, running each component twice in development. With Strict Mode off, we counted one line for each: 0, 10, 20.

Score never changed points. App passed a new value each time, and Score ran again to show it.

We also checked something smaller. When the component that writes <Score … /> renders again, it makes a new element with a new props object. That happens even if the values inside are the same. A child passed in as children is different. As Part 2 showed, its element stays the same object, so React can skip it. Part 18 explains what React does with that. Part 20 shows how to skip a child whose props did not change.

Passing all props at once

Sometimes a component takes its props and hands all of them to another component. You can pass a whole object as props with {...props}. In JavaScript, the three dots are called spread. Each key of the object becomes one prop.

function Avatar({ name, size }: { name: string; size: number }) {
  return <p>{name}, size {size}</p>
}

export default function App() {
  const avatarProps = { name: 'Ana', size: 100 }
  return (
    <div>
      <Avatar {...avatarProps} />
      <Avatar {...avatarProps} size={50} />
    </div>
  )
}

Run it. The first card gets name and size from the object. In the second card, size={50} comes after the spread. When the same prop appears twice, the last one wins, so the size is 50.

The order matters. Try <Avatar size={50} {...avatarProps} />. Now the spread comes last, so its size: 100 wins. TypeScript reports an error: 'size' is specified more than once, so this usage will be overwritten. The playground shows “Ana, size 100”.

Use spread with care. React’s docs warn against using it everywhere. It hides which props a component really gets. And it can pass things you didn’t mean to pass. Here a component spreads all of its props onto a <div>:

function Box(props: { title: string; isActive: boolean }) {
  return <div {...props}>A box</div>
}

export default function App() {
  return <Box title="Hello" isActive={true} />
}

TypeScript allows this. But press Run, and the Console shows a warning that starts like this:

React does not recognize the `isActive` prop on a DOM element.

title is a real HTML attribute, so it reached the page. isActive is not, so React warns you. Here it also left isActive off the page, because its value is true. With a text value, like isActive="yes", React still warns, but it does put isactive="yes" on the <div>. We checked both. The fix is to pass a <div> only the props it understands: <div title={props.title}>.

Common mistakes

Forgetting the curly braces when you take out a prop

function Greeting(name) {
  return <p>Hello, {name}!</p>
}

export default function App() {
  return <Greeting name="Ana" />
}

Without { }, the parameter name is not the name. It’s the whole props object, { name: "Ana" }. TypeScript complains that name has no type. The playground doesn’t check types, so press Run. React stops with this error:

Objects are not valid as a React child (found: object with keys {name}). If you meant to render a collection of children, use an array instead.

“Object with keys {name}” is the clue. You tried to show the props object itself. The fix: function Greeting({ name }: { name: string }).

Passing a number as text

function ProfileCard({ name, age }: { name: string; age: number }) {
  return <p>{name} is {age}. Next year: {age + 1}</p>
}

export default function App() {
  return <ProfileCard name="Ben" age="9" />
}

TypeScript catches this: Type 'string' is not assignable to type 'number'. In the playground, it runs with no error and no warning. But the page says “Ben is 9. Next year: 91”.

Quotes made age the text "9". In JavaScript, "9" + 1 sticks the two together and gives "91". The fix: age={9}, with curly braces.

Quotes around curly braces

name="{name}" passes the text {name}, not the variable. The page shows “Hello, {name}!”. You met the same mistake in Part 1, with value="{site}". Use name={name}.

Changing a prop

props.name = ... throws an error in development. In production, the change goes through with no error. Changing an object inside a prop, like player.points = ..., gets no error at all. Make a new variable instead, as in Props are read-only.

Spelling a prop name wrong

function Greeting({ name }: { name: string }) {
  return <p>Hello, {name}!</p>
}

export default function App() {
  return <Greeting nmae="Ana" />
}

TypeScript stops you: Property 'nmae' does not exist. In the playground, nothing warns you. The page says “Hello, !”, because name is undefined. This mistake is a good reason to use TypeScript in a real project. Part 10 sets one up.

Practice

Press Edit on any example above and try these.

  1. In “Try this first”, add a city prop to ProfileCard and show it. Pass a different city to each card.
  2. Make a Badge component with a label and an optional color. If the parent doesn’t pass a color, use 'green'. Show two badges: one with no color, and one with color="red".
  3. In the Card example, add a third card titled “Buttons”, with two <button> tags inside it.
  4. Fix the Score example under “Why the rule exists”, so that it doesn’t change player. What does the page show now?
Answers
  1. Add city: string to the type, and write <p>City: {props.city}</p>. Then pass it: <ProfileCard name="Ana" age={12} city="Lisbon" />. If you forget to pass city to one card, that card shows nothing after “City:”. TypeScript would say the prop is missing.
  2. See the code under this list. “New” is green, and “Sale” is red.
  3. <Card title="Buttons"><button>Yes</button><button>No</button></Card>. Card needs no change. Its children is now an array of two elements.
  4. Write const total = player.points + 5 and show {total}. The page shows “Ana: 15 points”, even with Strict Mode on. The ana object keeps its 10 points.

The Badge for task 2:

function Badge({ label, color = 'green' }: { label: string; color?: string }) {
  return <span style={{ backgroundColor: color, color: 'white', padding: 4 }}>{label}</span>
}

export default function App() {
  return (
    <p>
      <Badge label="New" /> <Badge label="Sale" color="red" />
    </p>
  )
}

Interview questions

Try to answer each one out loud before you open the answer.

What are props?

Props are the values a parent component passes to a child component. You write them like attributes in JSX: <ProfileCard name="Ana" age={12} />. React collects them into one object and passes it to the component as its argument. Most code takes the values out with destructuring: function ProfileCard({ name, age }).

A strong answer adds that a prop can be any JavaScript value, including objects, arrays and functions. It also knows two special names. key never reaches the props object. We checked: with <Item key="a" name="Ana" />, the props held only name. Reading props.key gave undefined, and React warned that key is not a prop. ref is different in React 19. A function component gets it as a normal prop, with no forwardRef needed. We checked: props.ref arrived, and it worked on an <input>.

What is the difference between props and state?

Props come from outside: the parent passes them in, and the child must not change them. State is data React keeps for that component’s place on the page. The component can change it with its set function. Part 4 covers state.

A strong candidate adds how the two meet. A child often gets a parent’s state as a prop, plus the function to change it. When the parent’s state changes, the parent renders again and passes new props down. Part 9 shows this.

Why are props read-only?

A component should give the same result every time it runs with the same inputs. Props are its main input. If a component changed its props, the result would depend on how many times it ran. React’s docs warn that changing props makes results differ from run to run. That makes bugs hard to find. In development, React freezes the props object, so changing it throws an error.

A strong answer adds the limits. The freezing is only in development, and only on the top level. An object inside a prop is not frozen. Changing it gives no error. In our test, Strict Mode’s second run made the bug easy to see: 20 points instead of 15.

What is children?

It’s the prop that holds whatever you put between a component’s opening and closing tags. With <Card><p>Hi</p></Card>, Card gets the <p> element as children, and shows it by writing {children}. So you can build frames like cards and panels. They don’t need to know what goes inside them. In TypeScript, its type is usually ReactNode.

A strong answer knows that children is not always an array. It’s undefined with nothing inside, one value with one child, and an array with several.

How do you give a prop a default value?

In the destructuring: function Avatar({ size = 100 }). In TypeScript, mark the prop optional with size?: number. The default is used when the prop is missing or undefined. It’s not used for null or 0.

Older code sets defaultProps on the component. A strong answer says React 19 removed defaultProps for function components. We checked: React 19.3 ignored it, and printed no warning.

What does <Button disabled /> pass?

disabled with the value true. A prop written with no value is short for ={true}. It’s useful for props that are on or off.

A strong candidate adds what happens on a real HTML tag. If the component passes it on, as <button disabled={disabled}>, React writes disabled="" on the page. We checked.

Can a child component change its parent’s data?

Not directly. Data flows one way in React, from parent to child. To let a child cause a change, the parent passes it a function as a prop. The child calls the function, and the parent updates its own state. Part 9 shows this.

A follow-up is often “What is prop drilling?” A value passes down through many layers of components. The middle ones don’t use it. They only pass it on, so it can reach one deep child. Context, in Part 23, is one way to avoid it.

Sources

  • Passing Props to a Component, react.dev: passing and reading props, destructuring, default values (missing or undefined only), spread syntax “with restraint”, children as a “hole”, and props as “read-only snapshots in time”.
  • Components and Hooks must be pure, react.dev: “Don’t mutate Props”, and why it gives inconsistent output.
  • Keeping Components Pure, react.dev: same inputs, same output, and treating props as read-only.
  • StrictMode, react.dev: why components run twice in development.
  • Thinking in React, react.dev: one-way data flow, from parent to child.
  • Passing Data Deeply with Context, react.dev: what prop drilling is.
  • Using TypeScript, react.dev: typing props and children with ReactNode.
  • @types/react 19.3.0, index.d.ts: what the ReactNode type includes.
  • React v19, react.dev blog: ref as a prop for function components, and no forwardRef needed.
  • React 19 Upgrade Guide, react.dev: defaultProps removed for function components.
  • The props objects, the typeof table, the children table, the default values, the frozen and production results, the Strict Mode counts and every error and warning above come from running React 19.3.0 and TypeScript 7.0.2 for this post.
  • This part follows the Components & Props kata in react-katas.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.