Skip to content
aviral gupta

// B2.3 · ~30 min · Beginner

Props and children

After this lesson you pass and read your own props, give them default values, forward them with spread, and wrap content with children.

Lesson 3 of 6 in B2 Describing the UI

You will be able to

  • Pass your own props and read them by destructuring or as props.name
  • Give a prop a default value, and forward all props with {...props}
  • Wrap nested JSX with children, and treat props as read-only
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson B2.2: curly braces and a style object. What text does Badge show?

    const person = {
      name: 'Hedy Lamarr',
      theme: { color: 'pink' }
    };
    
    function Badge() {
      return <p style={person.theme}>{person.name}'s badge</p>;
    }
  2. Predict · Activity 2 of 7

    Predict before you read on. App nests a paragraph inside <Card>. What text does App show?

    function Card({ title }) {
      return (
        <section>
          <h2>{title}</h2>
        </section>
      );
    }
    
    function App() {
      return (
        <Card title="Notes">
          <p>Buy milk</p>
        </Card>
      );
    }
  3. Practice · Activity 3 of 7

    Fill in the gap so the picture is 100 pixels wide when the parent passes no size.

    function Avatar({ name, ____ }) {
      return <img alt={name} width={size} />;
    }
    function Avatar({ name, }) {
  4. Practice · Activity 4 of 7

    Match each piece of code to what it does.

  5. Practice · Activity 5 of 7

    Profile forwards its props with spread. What text does App show?

    function Avatar({ name, size }) {
      return <p>{name} ({size})</p>;
    }
    
    function Profile(props) {
      return <Avatar {...props} />;
    }
    
    function App() {
      return <Profile name="Lin" size={80} />;
    }
  6. Brain teaser · Activity 6 of 7

    Brain teaser. Avatar has a default size of 100, and App passes size={null}. What text does App show?

    function Avatar({ size = 100 }) {
      return <p>{size}</p>;
    }
    
    function App() {
      return <Avatar size={null} />;
    }
  7. Apply · Activity 7 of 7

    Mini-task: a box with badges. In main.jsx, write a Badge component with the props label and color, where color defaults to gray, and a Box component with a heading prop that renders its children below the heading. The default export renders one Box with two Badges (one with a color, one without) and a paragraph inside it. Run it and check that the second badge is gray and the paragraph appears.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A card that wraps an avatar

Card has a title prop and renders whatever is nested inside it through children. Avatar reads a person object and a size, with size = 100 as the default. The first card gives no size, so the default applies; the second passes size={50} and nests a paragraph after the avatar. The pictures do not load in the runner; the snapshot shows the attributes React wrote.

main.jsx

function Avatar({ person, size = 100 }) {
  return (
    <img
      className="avatar"
      src={person.imageUrl}
      alt={person.name}
      width={size}
      height={size}
    />
  );
}

function Card({ title, children }) {
  return (
    <section className="card">
      <h2>{title}</h2>
      {children}
    </section>
  );
}

export default function Profile() {
  return (
    <div>
      <Card title="Default size">
        <Avatar person={{ name: 'Katsuko Saruhashi', imageUrl: 'katsuko.jpg' }} />
      </Card>
      <Card title="Size 50, with a note">
        <Avatar person={{ name: 'Aklilu Lemma', imageUrl: 'aklilu.jpg' }} size={50} />
        <p>Card shows whatever you nest inside it.</p>
      </Card>
    </div>
  );
}

Run it with

npm run dev

Output

<!-- #root -->
<div id="root"><div><section class="card"><h2>Default size</h2><img class="avatar" alt="Katsuko Saruhashi" width="100" height="100" src="katsuko.jpg"></section><section class="card"><h2>Size 50, with a note</h2><img class="avatar" alt="Aklilu Lemma" width="50" height="50" src="aklilu.jpg"><p>Card shows whatever you nest inside it.</p></section></div></div>
  • The first Avatar got no size, so size = 100 applied: width="100" height="100".
  • The second card received two children, the avatar and the paragraph, and rendered both where it wrote {children}.
  • Card does not know what it wraps: the same component holds a picture, or a picture and a note.
  • Each card renders its own title prop above its children.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Exercises

Exercise 1 of 2

A panel that wraps its content

Panel shows a title, but the paragraphs nested inside it never appear, and the second panel has an empty heading. Make Panel render its children below the heading, and give title the default value Notes. Keep the name Panel and its export: the tests render <Panel> with their own children.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Hints
  1. Hint 1

    Nested JSX arrives as a prop called children. Add it to the destructuring next to title.

  2. Hint 2

    Put {children} inside the <section>, after the <h2>.

  3. Hint 3

    A default goes into the destructuring: { title = 'Notes', children }.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function Panel({ title = 'Notes', children }) {
  return (
    <section className="panel">
      <h2>{title}</h2>
      {children}
    </section>
  );
}

export default function App() {
  return (
    <>
      <Panel title="Packing">
        <p>Bring a helmet</p>
      </Panel>
      <Panel>
        <p>Water the plants</p>
      </Panel>
    </>
  );
}
Run it on your computer

Install React 19.3 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.jsx

// Panel should show what is nested inside it, and the title Notes
// when no title is given.
export function Panel({ title }) {
  return (
    <section className="panel">
      <h2>{title}</h2>
    </section>
  );
}

export default function App() {
  return (
    <>
      <Panel title="Packing">
        <p>Bring a helmet</p>
      </Panel>
      <Panel>
        <p>Water the plants</p>
      </Panel>
    </>
  );
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import App, {Panel} from './main.jsx';

test('Panel shows the JSX nested inside it', () => {
  render(<Panel title="Tools"><button>Inside</button></Panel>);
  assert.ok(screen.queryByRole('button', {name: 'Inside'}), 'the button nested in <Panel> is not on the page');
});

test('A Panel without a title shows the title Notes', () => {
  render(<Panel><p>Hello</p></Panel>);
  const heading = screen.getByRole('heading', {level: 2}).textContent;
  assert.equal(heading, 'Notes', 'the heading reads ' + JSON.stringify(heading));
});

test('A title that is given is shown', () => {
  render(<Panel title="Packing"><p>Hello</p></Panel>);
  const heading = screen.getByRole('heading', {level: 2}).textContent;
  assert.equal(heading, 'Packing', 'the heading reads ' + JSON.stringify(heading));
});

test('App shows both panels with their content', () => {
  render(<App />);
  const headings = screen.getAllByRole('heading', {level: 2}).map((h) => h.textContent);
  assert.deepEqual(headings, ['Packing', 'Notes'], 'the headings are ' + JSON.stringify(headings));
  assert.ok(screen.queryByText('Water the plants'), 'the text Water the plants is not on the page');
});

In a Vite React project (npm create vite@latest, template react), put main.jsx in src/ as App.jsx and start it with npm run dev. React renders it inside <StrictMode>, as here.

Run the program:

npm run dev

There is no command for the checks on your computer yet. They are in main.test.jsx, written with node:test and React Testing Library.

Exercise 2 of 2

Forward props to an avatar

Gallery passes a person and sometimes a size to Profile, but Profile only passes person on, so no picture has a width. Make Profile forward every prop it gets to Avatar with the spread syntax, and give Avatar the default size 60. A size of 0 must stay 0. The pictures do not load in the runner; the tests check the attributes.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Hints
  1. Hint 1

    Run it first: the <img> elements have no width, because size never reaches Avatar.

  2. Hint 2

    Take the whole props object, function Profile(props), and render <Avatar {...props} />.

  3. Hint 3

    Write the default in Avatar's destructuring, size = 60. size || 60 would also replace 0, which a test catches.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

function Avatar({ person, size = 60 }) {
  return (
    <img
      className="avatar"
      src={person.imageUrl}
      alt={person.name}
      width={size}
      height={size}
    />
  );
}

export function Profile(props) {
  return (
    <div className="card">
      <Avatar {...props} />
    </div>
  );
}

export default function Gallery() {
  return (
    <>
      <Profile person={{ name: 'Lin Lanying', imageUrl: 'lin.jpg' }} size={80} />
      <Profile person={{ name: 'Aklilu Lemma', imageUrl: 'aklilu.jpg' }} />
    </>
  );
}
Run it on your computer

Install React 19.3 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.jsx

function Avatar({ person, size }) {
  return (
    <img
      className="avatar"
      src={person.imageUrl}
      alt={person.name}
      width={size}
      height={size}
    />
  );
}

// TODO: Avatar's size is 60 when none is given, and Profile passes
// every prop it gets on to Avatar.
export function Profile({ person }) {
  return (
    <div className="card">
      <Avatar person={person} />
    </div>
  );
}

export default function Gallery() {
  return (
    <>
      <Profile person={{ name: 'Lin Lanying', imageUrl: 'lin.jpg' }} size={80} />
      <Profile person={{ name: 'Aklilu Lemma', imageUrl: 'aklilu.jpg' }} />
    </>
  );
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import Gallery, {Profile} from './main.jsx';

test('Lin Lanying is 80 pixels wide, as Gallery passes', () => {
  render(<Gallery />);
  const width = screen.getByRole('img', {name: 'Lin Lanying'}).getAttribute('width');
  assert.equal(width, '80', 'the picture has width=' + JSON.stringify(width));
});

test('Aklilu Lemma gets the default size 60', () => {
  render(<Gallery />);
  const img = screen.getByRole('img', {name: 'Aklilu Lemma'});
  assert.equal(img.getAttribute('width'), '60', 'the picture has width=' + JSON.stringify(img.getAttribute('width')));
  assert.equal(img.getAttribute('height'), '60', 'the picture has height=' + JSON.stringify(img.getAttribute('height')));
});

test('Profile forwards any size and the picture', () => {
  render(<Profile person={{ name: 'Ada', imageUrl: 'ada.jpg' }} size={40} />);
  const img = screen.getByRole('img', {name: 'Ada'});
  assert.equal(img.getAttribute('width'), '40', 'the picture has width=' + JSON.stringify(img.getAttribute('width')));
  assert.equal(img.getAttribute('src'), 'ada.jpg', 'the picture has src=' + JSON.stringify(img.getAttribute('src')));
});

test('size={0} is passed on, not replaced by the default', () => {
  render(<Profile person={{ name: 'Ada', imageUrl: 'ada.jpg' }} size={0} />);
  const width = screen.getByRole('img', {name: 'Ada'}).getAttribute('width');
  assert.equal(width, '0', 'the picture has width=' + JSON.stringify(width));
});

In a Vite React project (npm create vite@latest, template react), put main.jsx in src/ as App.jsx and start it with npm run dev. React renders it inside <StrictMode>, as here.

Run the program:

npm run dev

There is no command for the checks on your computer yet. They are in main.test.jsx, written with node:test and React Testing Library.

Common mistakes

Changing a prop inside the component

function Avatar(props) {
  props.size = props.size * 2;
  return <img alt="Lin Lanying" width={props.size} />;
}

export default function Profile() {
  return <Avatar size={50} />;
}

What React prints

TypeError: Cannot assign to read only property 'size' of object '#<Object>'

Why, and the fix

Props are read-only. In development React freezes the props object, so the assignment throws and the run prints Uncaught TypeError (Chrome and Node wording; Firefox and Safari word it differently). Compute a new value instead: const shown = props.size * 2, and use width={shown}. If the size itself must change, the parent passes a different size.

Spreading props without the three dots

function Avatar({ size }) {
  return <img alt="Lin Lanying" width={size} />;
}

export default function Profile(props) {
  return <Avatar {props} />;
}

What React prints

error TS1005: '...' expected.

Why, and the fix

Braces in the attribute list of a tag must hold a spread, so the JSX transform stops and nothing runs. The full line starts with the position, main.jsx(6,19). Write <Avatar {...props} />, or pass props by name: size={props.size}.

Reading props.children after destructuring

function Card({ title }) {
  return (
    <section className="card">
      <h2>{title}</h2>
      {props.children}
    </section>
  );
}

export default function App() {
  return (
    <Card title="Notes">
      <p>Buy milk</p>
    </Card>
  );
}

What React prints

ReferenceError: props is not defined

Why, and the fix

With destructuring in the brackets, there is no variable called props: only title exists. Add children to the destructuring, function Card({ title, children }), and write {children}. Or keep the whole object, function Card(props), and use props.title and props.children.

React in the browser: React 19.3.0 (development build) and React Testing Library, MIT, served from this site; JSX is compiled by TypeScript 6.0.3, Apache-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Props are the one argument of a component

You already pass props to built-in tags: className, src, alt, width and height on an <img>. Their names are fixed by HTML. Your own components take any props you choose: <Avatar person={lin} size={100} />. React calls the component with one argument, an object holding every prop. You can keep it whole, function Avatar(props), and read props.size, or destructure it, function Avatar({ person, size }), and use size like a variable. Both read the same object; destructuring is the usual style because you rarely need the whole object.

Default values and forwarding with spread

Write = and a value after a prop in the destructuring: function Avatar({ person, size = 100 }). The default is used when the size prop is missing or undefined. size={null} and size={0} are values, so the default is skipped: null shows nothing and 0 is shown. A component that passes all its props on can forward them with the JSX spread syntax: <Avatar {...props} />. react.dev asks you to use spread with restraint; repeating props by name is often clearer, and many wrappers are better written with children.

children, and props you never change

JSX nested inside a tag, <Card><Avatar /></Card>, reaches Card as a prop called children. Card decides where it goes: <div className="card">{children}</div>. If Card does not render children, the nested JSX simply does not appear, with no warning. Props can differ from one render to the next, but each render gets a new, read-only props object. In development React freezes it, so props.size = 5 throws a TypeError. To show something else, a component asks its parent for different props; state, which you met in B1.3, is how a parent remembers them.

Sources

Last reviewed October 5, 2026