React · lesson 2 of 22
Beginner
5Intermediate
12- 6 Effects and useEffect
- 7 You Might Not Need an Effect
- 8 The Rules of Hooks
- 9 useReducer and useContext
- 10 Custom Hooks
- 11 Measuring Re-renders: memo, useMemo, and useCallback
- 13 Routing with React Router
- 14 Portals with createPortal
- 15 Error Boundaries
- 16 Suspense and lazy
- 17 Reading Promises with use, No Framework Needed
- 18 Retrying After an Error with react-error-boundary
Advanced
5Also part of React
5Props and Children
Pass data into components with props and default values, render arbitrary children, and count or map them with React's Children helpers.
By SkillByExample · Published
Props are the arguments a component receives; children is the one prop React fills in automatically from whatever sits between a component’s opening and closing tags. Every example still uses renderToString and npx tsx from the previous lesson’s project.
children Is Just a Prop
Nothing special happens to make children work — it’s filled in by JSX syntax, then read like any other prop.
// src/props-children/Panel.tsx
export function Panel({ heading, children }) {
return (
<section className="panel">
<h2>{heading}</h2>
{children}
</section>
);
}
// src/props-children/render-panel.tsx
import { renderToString } from "react-dom/server";
import { Panel } from "./Panel.js";
console.log(renderToString(<Panel heading="Build Status"><p>passing</p></Panel>));
console.log(renderToString(<Panel heading="Build Status">{null}</Panel>));
console.log(
renderToString(
<Panel heading="Server Load">
<p>cpu</p>
<p>mem</p>
</Panel>
)
);
$ npx tsx src/props-children/render-panel.tsx
<section class="panel"><h2>Build Status</h2><p>passing</p></section>
<section class="panel"><h2>Build Status</h2></section>
<section class="panel"><h2>Server Load</h2><p>cpu</p><p>mem</p></section>
A single <p>, null, and two sibling <p> tags all flow through the same children prop without Panel changing at all. null renders as nothing, which is why the second line’s <section> has no content after the heading.
Default Prop Values
Destructuring a prop with a default supplies it only when the caller omits that prop (or passes undefined).
// src/props-children/Avatar.tsx
export function Avatar({ name, size = "medium" }) {
return (
<span className={`avatar avatar-${size}`}>
{name.charAt(0).toUpperCase()}
</span>
);
}
// src/props-children/render-avatar.tsx
import { renderToString } from "react-dom/server";
import { Avatar } from "./Avatar.js";
console.log(renderToString(<Avatar name="Ada" />));
console.log(renderToString(<Avatar name="Grace" size="large" />));
$ npx tsx src/props-children/render-avatar.tsx
<span class="avatar avatar-medium">A</span>
<span class="avatar avatar-large">G</span>
Avatar name="Ada" has no size, so it falls back to "medium"; Avatar name="Grace" size="large" overrides it. The default only ever applies to a missing or undefined prop, not to any other falsy value.
Counting and Mapping children
children isn’t guaranteed to be an array — with exactly one child, it’s that child directly. Children.count and Children.map, from "react", treat every shape (nothing, one item, several items) the same way.
// src/props-children/ChildList.tsx
import { Children } from "react";
export function ChildList({ children }) {
return (
<div>
<p>{Children.count(children)} item(s)</p>
<ul>{Children.map(children, (child, i) => <li key={i}>{child}</li>)}</ul>
</div>
);
}
// src/props-children/render-child-list.tsx
import { renderToString } from "react-dom/server";
import { ChildList } from "./ChildList.js";
console.log(renderToString(<ChildList><span>one</span></ChildList>));
console.log(
renderToString(
<ChildList>
<span>one</span>
<span>two</span>
<span>three</span>
</ChildList>
)
);
$ npx tsx src/props-children/render-child-list.tsx
<div><p>1<!-- --> item(s)</p><ul><li><span>one</span></li></ul></div>
<div><p>3<!-- --> item(s)</p><ul><li><span>one</span></li><li><span>two</span></li><li><span>three</span></li></ul></div>
One <span> or three, Children.count reports the right number either way, and Children.map always returns an array ChildList can put inside <ul> — no branching on whether children happens to already be an array.
Practice
1. Add a footer prop to Panel that renders inside a <footer> when it’s given.
Solution
import { renderToString } from "react-dom/server";
function Panel({ heading, footer, children }) {
return (
<section className="panel">
<h2>{heading}</h2>
{children}
{footer && <footer>{footer}</footer>}
</section>
);
}
console.log(
renderToString(
<Panel heading="Build Status" footer={<small>updated 2m ago</small>}>
<p>passing</p>
</Panel>
)
);
$ npx tsx practice1.tsx
<section class="panel"><h2>Build Status</h2><p>passing</p><footer><small>updated 2m ago</small></footer></section>
2. Add a "small" size to Avatar and render both a size="small" avatar and one with no size at all.
Solution
import { renderToString } from "react-dom/server";
import { Avatar } from "./Avatar.js";
console.log(renderToString(<Avatar name="Ida" size="small" />));
console.log(renderToString(<Avatar name="Zoe" />));
$ npx tsx practice2.tsx
<span class="avatar avatar-small">I</span>
<span class="avatar avatar-medium">Z</span>
Avatar doesn’t need to know every valid size up front — it just interpolates whatever string it gets into the class name. Zoe still falls back to "medium".
3. Pass a plain string, not an element, as ChildList’s children and predict what Children.count reports.
Solution
import { renderToString } from "react-dom/server";
import { ChildList } from "./ChildList.js";
console.log(renderToString(<ChildList>just one string</ChildList>));
$ npx tsx practice3.tsx
<div><p>1<!-- --> item(s)</p><ul><li>just one string</li></ul></div>
A single string is one child, the same as a single element — Children.count treats both as 1, and Children.map wraps the string in its own <li>.