Understanding React children

The children prop lets a component render the JSX placed between its opening and closing tags. React also provides a Children API for the less common cases where a component needs to inspect or change that JSX.

The children prop

children is a normal prop. React fills it with the content between a component's tags:

import { type ReactNode } from 'react'
 
interface ListProps {
  children: ReactNode
}
 
function List({ children }: ListProps) {
  return <ul>{children}</ul>
}
 
function App() {
  return (
    <List>
      <li>Apple</li>
      <li>Banana</li>
    </List>
  )
}

Here, the two <li> elements become the children value passed to List. ReactNode is the usual TypeScript type when a prop may contain anything React can render, such as JSX, text, a number, null, or several of those values.

Render a list from data when you can

If your items already live in an array, use the array's map method. This is the simplest way to get each item's index:

import { type ReactNode } from 'react'
 
interface ListItemProps {
  children: ReactNode
  index: number
}
 
function ListItem({ children, index }: ListItemProps) {
  const color = index % 2 === 0 ? 'red' : 'blue'
 
  return (
    <li style={{ color }}>
      {children} {index}
    </li>
  )
}
 
function App() {
  const items = ['foo', 'bar', 'baz']
 
  return (
    <ul>
      {items.map((item, index) => (
        <ListItem key={item} index={index}>
          {item}
        </ListItem>
      ))}
    </ul>
  )
}

The result looks like this:

  • foo 0
  • bar 1
  • baz 2

This approach is clear because the item, key, and index are all passed in one place. For a complete example that filters an array while rendering a React list, see the filterable list component.

When Children.map is useful

Sometimes an API is easier to read when callers write each item as JSX:

<List>
  <ListItem>foo</ListItem>
  <ListItem>bar</ListItem>
  <ListItem>baz</ListItem>
</List>

The children value may look like an array here, but React does not promise that it is one. One child, many children, and no children can have different shapes. Use Children.map when you need to visit each child safely.

The next example adds an index prop to each child. cloneElement creates a new element with that extra prop; it does not change the original element.

import { Children, cloneElement, isValidElement, type ReactNode } from 'react'
 
interface ListItemProps {
  children: ReactNode
  index?: number
}
 
function ListItem({ children, index }: ListItemProps) {
  const color = index !== undefined && index % 2 === 0 ? 'red' : 'blue'
 
  return (
    <li style={{ color }}>
      {children} {index}
    </li>
  )
}
 
interface ListProps {
  children: ReactNode
}
 
function List({ children }: ListProps) {
  const indexedChildren = Children.map(children, (child, index) => {
    if (!isValidElement<ListItemProps>(child)) return child
 
    return cloneElement(child, { index })
  })
 
  return <ul>{indexedChildren}</ul>
}

isValidElement is important. A React child can be text, null, or another renderable value that cloneElement cannot clone.

The JSX-only version produces the same result:

  • foo 0
  • bar 1
  • baz 2

The five Children methods

  • Children.map(children, fn) returns a new list after running fn for each child
  • Children.forEach(children, fn) visits each child but does not return a list
  • Children.count(children) returns the number of children React sees
  • Children.only(children) returns one React element and throws an error if it receives anything else
  • Children.toArray(children) returns a flat array that you can filter, sort, or reverse

These methods only inspect the JSX passed to the component. They do not render another component and look inside its result. For example, if <MoreItems /> renders three list items, Children.count still sees <MoreItems /> as one child.

Fragments are another sharp edge: Children does not visit the items inside a Fragment. These limits can make code based on the exact number or order of children surprising.

Avoid matching children by component type

It is possible to compare child.type with a component:

if (isValidElement(child) && child.type === ListItem) {
  // This is a ListItem element in this exact form
}

This check is easy to break. It may stop matching after the component is wrapped by another component. It also cannot see components returned from inside another child. TypeScript checks the code you write, but it does not fix these limits at runtime.

Adding a private prop such as __TYPE has the same problem and exposes an implementation detail to every caller. Prefer an explicit prop or a different component API instead.

Prefer explicit data for anything complex

Children and cloneElement are still part of React, but the React docs call them uncommon and warn that they can lead to fragile code. If a component needs to filter items, reorder them, attach several values, or recognize different item types, pass structured data instead:

interface Item {
  id: string
  label: string
}
 
interface ListProps {
  items: Item[]
}
 
function List({ items }: ListProps) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={item.id}>
          {item.label} {index}
        </li>
      ))}
    </ul>
  )
}

Now items is a real array, each item has a clear shape, and the data flow is easy to follow. Context or a render prop can also be a better fit when nested components need information from their parent.

Use the lowercase children prop freely. Reach for the uppercase Children API only when a component truly needs to work with the JSX it receives, and use cloneElement sparingly.

For the full details, see the React reference pages for Children and cloneElement.