Blog

Part 44 · Children as Data in React

The children prop holds plain data you can read. Learn what it holds, React’s Children API and its traps, and the simpler ways React’s docs suggest instead.

In Part 3 you met children: whatever you put between a component’s tags. Most components just show it, with {children}. But children is a prop like any other. Its value is plain data, and a component can read it.

Part 2 showed that each JSX tag makes an element, a plain object. So children is often a list of objects. A component can look at them, count them, change their order or put something between them.

This part shows what children can hold, and how to read it. Then it covers React’s Children API, which does this for you, and the traps in it. Part 38 warned about one of them. At the end we look at the other ways that React’s docs suggest instead.

Try this first

Read this code. Don’t press Run yet.

import type { ReactNode } from 'react'

function Peek({ children }: { children?: ReactNode }) {
  console.log(Array.isArray(children) ? 'an array' : typeof children)
  return <div>{children}</div>
}

export default function App() {
  return (
    <>
      <Peek><b>A</b><b>B</b></Peek>
      <Peek><b>A</b></Peek>
      <Peek>Hello</Peek>
      <Peek />
    </>
  )
}
an array
an array
object
object
string
string
undefined
undefined

Peek logs what kind of value its children is. Array.isArray(x) is true when x is an array. typeof x gives the kind of value as text, like "string".

Make a guess. The first Peek gets two children, so children is an array. What will the second one log? It gets only one child.

Press Run, and look at the Console.

The first Peek logs “an array”. The second logs “object”: one child is not an array of one. It is the element itself. The third logs “string”, and the last logs “undefined”, because nothing is there.

Each line appears twice. That is Strict Mode, which runs each component twice while you develop. Part 2 explained it.

So children changes shape with what you put inside. Code that expects an array works for two children and breaks for one. Keep that in mind for the rest of this part.

children is just a prop

You never write children=. React fills it in for you. These two lines make elements with the same type and props:

import type { ReactNode } from 'react'

function Box({ children }: { children: ReactNode }) {
  return <div>{children}</div>
}

const a = <Box><b>Hi</b></Box>
const b = <Box children={<b>Hi</b>} />

Because it is a prop, children can hold any value that you can put inside curly braces. We wrote each case between <Box> and </Box> and logged children with React 19.3:

Between the tags children is
nothing undefined
Hello the text "Hello"
{42} the number 42
<b>Hi</b> one element (an object)
<p>One</p><p>Two</p> an array of 2 elements
<><p>One</p><p>Two</p> one element: the Fragment
{null}, {false}, {true} null, false, true
{() => 1} a function
<li>first</li>{list.map(...)} an array of 2: an element, then the array from map

Three rows need a closer look.

  • A Fragment is one child. <> and “ make one element. Its own children sit inside its props, not in the outer list.
  • Arrays can sit inside the array. In the last row, list.map(...) makes an array. That array goes into children as one item. So children is an array that holds another array.
  • A function is allowed. React can’t show a function on the page. But a component can call one. Part 40 used this for render props. The Children functions below skip a function: they never pass it to you, and they don’t count it.

Elements are objects you can read

Each element in children is a plain object. Part 2 showed its two main parts: type and props. There is a third one you can read: key.

Here a component reads all three from its one child:

import type { ReactElement } from 'react'

function Inspect({ children }: { children: ReactElement<{ className?: string }> }) {
  return (
    <ul>
      <li>type: {String(children.type)}</li>
      <li>key: {String(children.key)}</li>
      <li>className: {children.props.className}</li>
    </ul>
  )
}

export default function App() {
  return (
    <Inspect>
      <p key="first" className="big">Hello</p>
    </Inspect>
  )
}

Run it. The page shows type: p, key: first and className: big. The child itself, the paragraph, never shows. Inspect only reads it.

For an HTML tag, type is the tag’s name as text. For a component you wrote as a function, type is that function. A component wrapped in memo or lazy is different: its type is an object, not your function. We checked both. For a Fragment, it is a special value that React keeps in Fragment. So child.type === Fragment tells you that a child is a Fragment.

You can read an element, but you can’t change it. As Part 1 showed, React freezes elements while you develop. We checked: an element and its props are both frozen in React 19.3.

How do you know that a child is an element at all? It could be text or a number. React has a function for this, isValidElement. It is on React’s list of legacy functions too, as you’ll see below. It returns true for an object made by JSX. It returns false for text, numbers, null and arrays. We tried all four.

The Children API

Reading one child is easy. Reading many is not, because of what “Try this first” showed. children can be one thing, an array, or an array inside an array.

React has a set of functions that hide all of this. They live in an object called Children, with a capital C:

import { Children } from 'react'

Each one takes children as it comes, whatever its shape:

Function What it does
Children.count(children) gives the number of children
Children.forEach(children, fn) calls fn for each child
Children.map(children, fn) calls fn for each child, and makes a new list from what it returns
Children.only(children) gives back the one child, or throws an error
Children.toArray(children) makes a plain, flat array

A flat array has no arrays inside it. All the children sit in one row.

Before you use them, know where React puts them. The React docs list Children with the “Legacy React APIs”, next to cloneElement, createElement and isValidElement. The page says these “are not recommended for use in newly written code”. Legacy means old, kept so that old code still works. The Children page opens with a warning: “Using Children is uncommon and can lead to fragile code.” Fragile code breaks easily, when something small changes.

Don’t mix up the two words. The same page says the children prop, with a small c, “is good and encouraged”. Only the Children functions are the problem.

So why learn them? You will meet them in older code and in libraries. And the traps teach you how children really works.

Children.map

Here is a list that puts each child in its own <li>:

import { Children, type ReactNode } from 'react'

function List({ children }: { children: ReactNode }) {
  return <ul>{Children.map(children, child => <li>{child}</li>)}</ul>
}

export default function App() {
  return (
    <>
      <List>
        <b>Apples</b>
        <b>Bread</b>
      </List>
      <List>
        <b>Only milk</b>
      </List>
    </>
  )
}

Run it. Both lists work: the one with two children and the one with one.

Look at the <li>. It has no key. Part 8 said that items in a list need one. Yet React prints no warning here. That is because Children.map adds a key to each item for you. It builds the key from the child’s position, or from the child’s own key. We measured the keys it made, with React 19.3:

Children Keys after Children.map
<p>a</p><p>b</p> .0, .1
<p key="x">a</p><p>b</p> .$x, .1
<p>a</p>{['m', 'n'].map(...)}, with keys m and n .0, .1:$m, .1:$n

If your function returns one element with its own key, like <li key="r">, your key goes first: r/.0, r/.1.

You don’t need to read these keys. But you can see how they are made. A number is a position. $ marks a key that you gave. : means the child came from an array inside the array.

React’s docs say the children data structure “is considered opaque”. Opaque means you can’t see through it. Here it means: don’t depend on its shape. Without Children, you would write children.map(...) yourself. That fails in two ways. It crashes for one child, as Common mistakes shows. And with no key on the <li>, React prints Part 8’s key warning. We checked both.

Children.count and Children.toArray

count gives a number, and toArray gives a flat array. They seem to agree. On simple children they do. On other children they don’t. We measured both, with React 19.3:

Children count toArray length
<p>A</p><p>B</p> 2 2
<><p>A</p><p>B</p> 1 1
Hello, {name}! 3 3
<p>A</p>{false}<p>C</p> 3 2
{false} alone 1 0
{null} alone 0 0
<p>A</p>{list.map(...)}, two items 3 3
<MoreRows />, which shows two <p> 1 1
{() => 1} 0 0
<p>A</p>{() => 1} 1 1

What each row teaches:

  • A Fragment counts as one. Children doesn’t look inside it.
  • Text in pieces counts in pieces. Hello, {name}! is three children, as Part 2 showed.
  • false counts, but toArray drops it. The docs say toArray leaves out empty values: null, undefined, true and false. count keeps all of them when they sit in a list. We checked: <p>A</p>{null}<p>C</p> counts 3 too. So a hidden child still counts.
  • null alone counts 0. When children itself is null or undefined, every Children function stops at once. count gives 0 and toArray gives []. map gives back the same null or undefined, not an array. forEach never calls you, and only throws.
  • A function is skipped. {() => 1} alone counts 0. Next to a <p>, the count is 1.
  • Arrays inside are opened up. The two items from map count one by one.
  • A component counts as one. Children never calls MoreRows, so it can’t know that it shows two paragraphs.

The last row is the big one. React’s docs say: “There is no way to get the rendered output of an inner component like <MoreRows />“. We checked: while Children.count ran, MoreRows had been called 0 times. React calls it later, when it renders the page.

Children.forEach

forEach calls your function for each child, and returns nothing. Use it to build your own list or to collect data.

Watch what it does with false. For <p>A</p>{false}<p>C</p>, it calls your function three times. The second time, the child is null. React turns true, false and undefined into null before it calls you. Children.map does the same. So a hidden child still reaches your function, as null.

Children.only

Some components need exactly one child, and one element, not text. Children.only checks that. It returns the child, or it throws this error:

React.Children.only expected to receive a single React element child.

We passed it seven things. It threw for two elements, for text, for undefined, for null, and for an array that holds one element. It passed for one element, and for a Fragment. A Fragment is one element, even with ten children inside.

Example: a list with separators

Now let’s use what we know. We want a row of words with a dot between them, like Home · Shop · Shoes. A dot like this is called a separator: something that sits between items. The person using it should only write the items:

import { Children, type ReactNode } from 'react'

function Separated({ children }: { children: ReactNode }) {
  return (
    <p>
      {Children.map(children, (child, index) =>
        index === 0 ? child : [<span key="dot"> · </span>, child]
      )}
    </p>
  )
}

export default function App() {
  return (
    <Separated>
      <b>Home</b>
      <b>Shop</b>
      <b>Shoes</b>
    </Separated>
  )
}

Run it. The page shows Home · Shop · Shoes.

For the first child, the function returns the child itself. For every other child, it returns an array of two: a dot, then the child. Children.map puts all of them into one flat list.

Every dot has the same key, "dot". That is fine here. The docs say that keys you return in an array “only need to be unique locally amongst each other”. Children.map puts the child’s own key in front of them. We measured: the second dot’s key became .1/.$dot, and the third’s became .2/.$dot. The child next to it, with no key of its own, got its position: .1/.1. React printed no warning.

Here is the same thing, step by step.

1. You write three <b> tags between the Separated tags. 2. JSX makes children an array of three element objects. 3. Children.map calls your function for each child and its index. 4. Index 0 gives the child. The others give a dot and the child. 5. Children.map makes one flat list, and gives each item a key. 6. React shows the list: Home · Shop · Shoes. <Separated><b>Home</b><b>Shop</b><b>Shoes</b></Separated> [{ type: "b", key: null, props: { children: "Home" } }, { … }, { … }] fn(<b>Home</b>, 0) fn(<b>Shop</b>, 1) fn(<b>Shoes</b>, 2) <b>Home</b> [ ·, <b>Shop</b> ] [ ·, <b>Shoes</b> ] .0 Home .1/.$dot · .1/.1 Shop .2/.$dot · .2/.1 Shoes Home · Shop · Shoesthe page now has <p><b>Home</b><span> · </span><b>Shop</b>…</p>

From JSX children to a list with separators. Press play, or step through it.

  1. You write three <b> tags between <Separated> and </Separated>.
  2. JSX makes children an array of three element objects. None of them has a key.
  3. Children.map calls your function three times. It gets each child and its index, which is its position: 0, 1, 2.
  4. Your function returns the first child alone. For the others, it returns a dot and the child.
  5. Children.map puts it all in one flat list, and gives each item a key.
  6. React shows the list: Home · Shop · Shoes.

A hidden child still gets a dot

Now say one item shows only sometimes. Here, a button hides “Shop”:

import { Children, useState, type ReactNode } from 'react'

function Separated({ children }: { children: ReactNode }) {
  return (
    <p>
      {Children.map(children, (child, index) =>
        index === 0 ? child : [<span key="dot"> · </span>, child]
      )}
    </p>
  )
}

export default function App() {
  const [showShop, setShowShop] = useState(true)
  return (
    <>
      <button onClick={() => setShowShop(!showShop)}>Hide or show Shop</button>
      <Separated>
        <b>Home</b>
        {showShop && <b>Shop</b>}
        <b>Shoes</b>
      </Separated>
    </>
  )
}

{showShop && <b>Shop</b>} shows the item only when showShop is true. Part 7 covered &&.

Run it and press the button. The page shows Home · · Shoes, with two dots.

When showShop is false, the middle child is false. As you saw, Children.map still calls your function for it, with null. The index is 1, so the function returns a dot and null. The dot shows, and the null shows nothing.

The fix is to drop the empty children first. Children.toArray does that. Then the index counts only the children that are really there.

This time each item is a button that counts its own clicks. That lets us check one more thing: does an item keep its state when another item hides?

import { Children, useState, type ReactNode } from 'react'

function Separated({ children }: { children: ReactNode }) {
  const items = Children.toArray(children)
  return (
    <p>
      {items.flatMap((child, index) =>
        index === 0 ? [child] : [<span key={'dot' + index}> · </span>, child]
      )}
    </p>
  )
}

function Item({ name }: { name: string }) {
  const [clicks, setClicks] = useState(0)
  return <button onClick={() => setClicks(clicks + 1)}>{name} {clicks}</button>
}

export default function App() {
  const [showShop, setShowShop] = useState(true)
  return (
    <>
      <button onClick={() => setShowShop(!showShop)}>Hide or show Shop</button>
      <Separated>
        <Item name="Home" />
        {showShop && <Item name="Shop" />}
        <Item name="Shoes" />
      </Separated>
    </>
  )
}

Run it. Click “Shoes” twice, so it shows 2. Then press “Hide or show Shop”. The page shows Home 0 · Shoes 2. One dot, and Shoes kept its count.

Two things make this work.

  • toArray gives each child a key. Shoes gets .2, from its place in the JSX. false still holds place 1, so Shoes keeps .2 when Shop hides. React finds it by that key, so its state stays.
  • flatMap makes one flat list. flatMap is like map, but it opens up each array that your function returns. So the dots and the items sit side by side, each with its own key. Each dot gets 'dot' + index, so no two keys are the same.

Why not plain items.map, returning [dot, child] as before? Then the list holds small arrays inside it. React matches arrays inside a list by their position, not by a key. When Shop hides, Shoes moves from place 2 to place 1, so React builds it again. We tried it: Shoes went from 2 back to 0. With flatMap, it stayed at 2. The first Children.map version kept it too, because Children.map gives keys like .2/.1. It just showed two dots.

Example: numbered steps, and the wrapper problem

Here is a component that numbers its children. Look at the last child, <MoreSteps />. It is a component that shows two steps of its own:

import { Children, type ReactNode } from 'react'

function Steps({ children }: { children: ReactNode }) {
  return (
    <div>
      {Children.map(children, (child, index) => (
        <section>
          <b>Step {index + 1}</b>
          {child}
        </section>
      ))}
    </div>
  )
}

function MoreSteps() {
  return (
    <>
      <p>Pour the tea.</p>
      <p>Add milk.</p>
    </>
  )
}

export default function App() {
  return (
    <Steps>
      <p>Boil the water.</p>
      <p>Add the tea leaves.</p>
      <MoreSteps />
    </Steps>
  )
}

Make a guess before you run it. The page will show four sentences. How many step numbers will it show?

Run it. Three. “Pour the tea.” and “Add milk.” share Step 3.

Steps sees three children: two <p> elements and one <MoreSteps /> element. It numbers what it sees. It can’t see that MoreSteps will show two paragraphs, because React hasn’t called MoreSteps yet. That happens later, when React renders Steps‘s result.

This is the warning that Part 38 quoted. Any code that reads children stops at the first component. So the person using Steps can’t move steps into a component of their own without breaking the numbers. React’s docs say this “makes it hard to extract a component”. To extract a component means to move some JSX into a new component.

Wrapping the two steps in a Fragment, <>..., breaks it in the same way. A Fragment is one child too.

Example: finding special children by type

Sometimes a parent wants only some of its children. Here, Tabs looks for <Tab> children. It reads each one’s label to make the buttons, and shows the content of the chosen one:

import { Children, isValidElement, useState, type ReactNode } from 'react'

type TabProps = { label: string; children: ReactNode }

function Tab({ children }: TabProps) {
  return <>{children}</>
}

function Tabs({ children }: { children: ReactNode }) {
  const [current, setCurrent] = useState(0)
  const tabs: TabProps[] = []
  Children.forEach(children, child => {
    if (isValidElement<TabProps>(child) && child.type === Tab) {
      tabs.push(child.props)
    }
  })
  return (
    <div>
      {tabs.map((tab, index) => (
        <button key={tab.label} onClick={() => setCurrent(index)}>
          {tab.label}
        </button>
      ))}
      <div>{tabs[current]?.children}</div>
    </div>
  )
}

function ReturnsTab() {
  return <Tab label="Returns">You have 30 days.</Tab>
}

export default function App() {
  return (
    <Tabs>
      <Tab label="Shipping">We ship in 2 days.</Tab>
      <Tab label="Payment">We take cards.</Tab>
      <ReturnsTab />
    </Tabs>
  )
}

isValidElement<TabProps>(child) checks that child is an element. It also tells TypeScript what its props hold, so child.props.label is a string. But that part is only a promise, like as. Nothing checks the props while the app runs. The check child.type === Tab is what makes it safe. Without it, child.props has the type unknown. Then TypeScript refuses tabs.push(child.props): Argument of type 'unknown' is not assignable to parameter of type 'TabProps'. tabs[current]?.children reads children only if tabs[current] exists. The ?. gives undefined instead of an error.

Run it. You see two buttons, not three. Click “Payment”, and its text shows.

The “Returns” tab is gone, and no error tells you why. ReturnsTab returns a <Tab>, but Tabs never sees that. It sees an element whose type is ReturnsTab, not Tab. So child.type === Tab is false, and the tab is skipped.

The same thing happens with any wrapper. Wrap a <Tab> in a <div>, or in memo(Tab), and its type is no longer Tab.

These tabs are kept short. They are not ready for screen readers or the keyboard. Part 39 builds proper ones.

What to use instead

The Children page lists other ways that avoid these traps. Here are three.

Pass the data as an array prop

Give the component an array of objects. Then it uses the normal array map, with no Children at all:

import { useState, type ReactNode } from 'react'

type TabData = { id: string; label: string; content: ReactNode }

function Tabs({ tabs }: { tabs: TabData[] }) {
  const [current, setCurrent] = useState(0)
  return (
    <div>
      {tabs.map((tab, index) => (
        <button key={tab.id} onClick={() => setCurrent(index)}>
          {tab.label}
        </button>
      ))}
      <div>{tabs[current]?.content}</div>
    </div>
  )
}

const returnsTab: TabData = { id: 'returns', label: 'Returns', content: <p>You have 30 days.</p> }

export default function App() {
  return (
    <Tabs
      tabs={[
        { id: 'shipping', label: 'Shipping', content: <p>We ship in 2 days.</p> },
        { id: 'payment', label: 'Payment', content: <p>We take cards.</p> },
        returnsTab,
      ]}
    />
  )
}

Run it, and click “Returns”. All three tabs are there this time.

The “Returns” tab was made somewhere else, in returnsTab, and it still works. An array can be built anywhere: in a variable, in a function, from a server’s data. TypeScript checks every object in it. And tabs.length is always the true count.

React’s docs say this way “lets you associate some extra data like header with each item”. Here the extra data is label and id.

Pass a function: render props

A component can take a function and call it for each item. Part 40 built a list with a renderItem prop. The list owns the loop and the keys. You choose what each item looks like. React’s docs call a prop like this “a regular prop which happens to be a function”.

Give the user the parts, and share state with context

The docs’ first suggestion is to “export a Row component”. The user wraps each row in it. Then the parent doesn’t need to change its children at all.

The parts often need shared state. The Children page doesn’t cover that, but the cloneElement page does: “Another alternative to cloneElement is to pass data through context.”

This is what Part 38 built: an accordion with Accordion.Item, Accordion.Trigger and Accordion.Panel. Each part reads what it needs from context. Context reaches through any component in between. So the user can move parts into components of their own, and nothing breaks.

Part 43 used cloneElement, the other Children-style tool. It makes a copy of a child with new props. It is on the same “Legacy React APIs” list, and its page has the same warning about fragile code. Its page is also where React suggests context.

TypeScript: typing children

React’s types give you three common choices.

  • ReactNode is anything React can show: elements, text, numbers, arrays, null, undefined, true and false. It also covers a few less common things, like big numbers (bigint), portals and promises. Use it for most components.
  • ReactElement is one JSX element only. No text, and no list of elements.
  • PropsWithChildren<P> takes your props type P and adds children?: ReactNode to it.

Here is PropsWithChildren in use:

import type { PropsWithChildren } from 'react'

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

const empty = <Card title="Empty" />
const full = <Card title="Full">Hello</Card>

The ? makes children optional, so <Card title="Empty" /> is allowed. With children: ReactNode and no ?, TypeScript would stop you: Property 'children' is missing.

Typing “only one child”

To ask for exactly one element, use ReactElement. Then TypeScript stops wrong uses before they run:

import type { ReactElement } from 'react'

function Frame({ children }: { children: ReactElement }) {
  return <div className="frame">{children}</div>
}

const two = (
  <Frame>
    <b>A</b>
    <b>B</b>
  </Frame>
)

TypeScript’s error starts with This JSX tag's 'children' prop expects a single child. It ends with but multiple children were provided. Text gets a different message: 'Frame' components don't accept text as child elements.

TypeScript can’t check which element, though. React’s TypeScript page says it plainly: “you cannot use TypeScript to describe that the children are a certain type of JSX elements”. So you can’t say “only <Tab> children”. And a component like <ReturnsTab /> is a fine ReactElement, whatever it shows.

The playground doesn’t check types. If the rule must hold there too, check it while the app runs, with Children.only. It has the same limit: it checks for one element, not which element.

Common mistakes

Assuming children is always an array

import type { ReactNode } from 'react'

function List({ children }: { children: ReactNode }) {
  const items = children as ReactNode[]
  return <ul>{items.map((child, i) => <li key={i}>{child}</li>)}</ul>
}

export default function App() {
  return (
    <List>
      <b>Only one</b>
    </List>
  )
}

children as ReactNode[] tells TypeScript “trust me, this is an array”. It doesn’t change the value. Without it, TypeScript refuses children.map. Its first error is 'children' is possibly 'null' or 'undefined'. The next is Property 'map' does not exist on type. Both are right.

Press Run. The app stops with items.map is not a function. One child is not an array, as “Try this first” showed. With no children at all, the message is Cannot read properties of undefined (reading 'map').

The fix: use Children.map(children, ...), or Children.toArray(children) and then map. Better still, take an array prop.

Forgetting keys in your own list

React’s own Children page builds a separated list with forEach and <hr key={index} />. Leave the key out:

import { Children, type ReactNode } from 'react'

function SeparatorList({ children }: { children: ReactNode }) {
  const result: ReactNode[] = []
  Children.forEach(children, child => {
    result.push(child)
    result.push(<hr />)
  })
  result.pop()
  return <div>{result}</div>
}

export default function App() {
  return (
    <SeparatorList>
      <p>A</p>
      <p>B</p>
      <p>C</p>
    </SeparatorList>
  )
}

Press Run. The page looks right. But the Console shows Part 8’s warning: Each child in a list should have a unique "key" prop. result is an array you made yourself, so each item in it needs a key. result.pop() removes the last item, the extra line at the end.

The fix: give each <hr> a key, like key={index}, from forEach‘s second argument. Or use Children.map, which adds keys for you.

Counting children to decide something

Children.count(children) counts what was written, not what shows. A Fragment counts as 1. {false} counts as 1. A component counts as 1, whatever it shows. Don’t use the count to choose a layout or to say “3 items”. Count the data instead, with an array prop and tabs.length.

Reading child.type for a component that may get wrapped

You saw it in the tabs. A <Tab> inside any other component, or inside memo, is skipped with no error. If you must check type, say clearly that the special children must be direct children. Better: use an array prop, or the context of Part 38.

Writing Children when you mean children

children, small c, is the prop. Children, capital C, is React’s object of functions. {children} in your JSX is good. Children.map is the one to think twice about.

Practice

Press Edit on the examples above and try these.

  1. In “Try this first”, add <Peek>{false}</Peek> and <Peek>{[1, 2]}</Peek> at the end. What does each one log?
  2. In the numbered steps, take <MoreSteps /> out. Put its two paragraphs inside a Fragment, <>..., in its place. How many step numbers do you see now?
  3. In the first separator example, add {false} before <b>Home</b>. What does the page show? Then add {false} before <Item name="Home" /> in the flatMap version.
  4. In the “array prop” tabs, add a fourth tab, “Gifts”, with the text We wrap gifts., made in a function.
Answers
  1. <Peek>{false}</Peek> logs “boolean” twice. <Peek>{[1, 2]}</Peek> logs “an array” twice. Each line comes twice because of Strict Mode. The page shows 12 for the second one, and nothing for the first.
  2. Still three. The Fragment is one child, so Step 3 holds both paragraphs.
  3. The page shows · Home · Shop · Shoes, with a dot at the start. false is at index 0, so Home is at index 1 and gets a dot. The flatMap version drops false first, with toArray, so it shows Home 0 · Shop 0 · Shoes 0.
  4. For example, function makeTab(id: string, label: string, text: string): TabData { return { id, label, content: <p>{text}</p> } }, then add makeTab('gifts', 'Gifts', 'We wrap gifts.') to the array. Four buttons show, and “Gifts” shows its text when you click it.

Interview questions

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

What can the children prop hold?

Anything you can put inside curly braces. That means text, a number, one element, or an array of these. It can also be null, undefined, true, false, or a function. Its shape depends on what was written between the tags. Nothing gives undefined. One child gives that child, not an array. Several children give an array, which can hold arrays of its own.

A strong answer says React’s docs warn against this. Code shouldn’t depend on the shape of children.

Why does children.map sometimes crash?

With one child, children is that child, not an array of one. Text and elements have no map method, so it throws children.map is not a function. With no child, it is undefined. Children.map and Children.toArray work with every shape. TypeScript already refuses children.map on a ReactNode.

What does Children.count return for a Fragment, for {false}, and for a component?

1 for each. Children doesn’t look inside a Fragment, it counts empty values like false, and it never calls a component. So <MoreRows /> counts as 1 even if it shows ten rows, or none. Children.toArray drops null, undefined, true and false, so its length can differ from the count.

A strong answer adds one more case. When children is just null or undefined, the count is 0. Every Children function stops early on it. In a list, null counts 1 like false.

Why is the Children API called fragile?

It only sees the JSX that was written between the tags. It can’t see what a child component will render. So code that counts, numbers or filters children can break. It breaks when the user moves some children into a component, or wraps them. React’s docs: “There is no way to get the rendered output of an inner component”. React lists Children with its legacy APIs, “not recommended for use in newly written code”.

What does React suggest instead of the Children API?

Three things. Give the user a part component, like Row, and let them wrap each item. Shared state then goes through context, as in compound components. React’s cloneElement page suggests that part. Or take an array of objects as a prop, like tabs={[{ id, label, content }]}. Or take a render prop, a function the component calls for each item.

A strong answer says how to choose. An array prop fits data. Parts with context fit layouts the user wants to change. A render prop fits a list whose items the user wants to draw.

How do you type children in TypeScript, and how do you ask for only one child?

ReactNode for anything React can show. PropsWithChildren<P> adds an optional children?: ReactNode to your props. For exactly one element, use ReactElement. Then TypeScript refuses text and refuses two children.

A strong answer says the limits. TypeScript can’t require one kind of element, like only <li>. React’s docs say so. And a type check doesn’t run in the browser, so Children.only is the run-time check.

What keys does Children.map give its results?

It builds each key from the original child. .0 and .1 are positions. .$x is a child with key="x". .1:$m is an item from an array inside the array. If your function returns one element with a key, your key goes first: r/.0. If it returns an array, the child’s key goes first: .1/.$dot for a keyed item, .1/.1 for one without a key. So the children don’t lose their keys when you wrap them, and you don’t get a key warning.

Sources

  • Children, react.dev: the five functions, what each one counts and leaves out, the keys, “opaque”, the pitfalls, the MoreRows example, and the three alternatives.
  • Legacy React APIs, react.dev: Children, cloneElement, createElement and isValidElement are on it, and the list is “not recommended for use in newly written code”.
  • isValidElement, react.dev: what counts as an element.
  • cloneElement, react.dev: the same pitfall, for Part 43’s tool, and passing data through context.
  • Using TypeScript, react.dev: ReactNode, ReactElement, and that TypeScript can’t require a kind of element.
  • Passing Props to a Component, react.dev: children is a prop.
  • What children holds, the counts, the keys, the error messages and the TypeScript errors above come from running React 19.3.0 and TypeScript 7.0.2 for this post.
  • This part follows the Children as Data 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.