Skip to main content

📦 Children Props

You already know that whatever you nest between a component's tags becomes its children. That's the doorway. In this lesson we walk through it and explore the whole room: how children can be text, elements, or even a function, and the small toolkit React gives you for inspecting and transforming it to build genuinely flexible components.

Week 4 · Day 5 (Friday: Component Composition) · Lecture 2

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what the children prop is and the shapes it can take
  • Build reusable wrapper and layout components that accept arbitrary content
  • Use the React.Children utilities (map, count, toArray, only) safely
  • Inject props into children with cloneElement and know when not to
  • Apply the children-as-a-function (render-prop) pattern
  • Compose compound components such as an accordion using shared context

Estimated Time: 60 minutes

Practice: Build a <List> that auto-styles its items and a small context-driven accordion.

In This Lesson

The children Prop

Every JSX element you write is really a call to React.createElement. When you nest content between an opening and closing tag, React collects that content and hands it to the component under one special prop name: children. You never pass it as an attribute — the JSX nesting is how you pass it.

graph TD A["<Card> ... </Card>"] --> B[React collects the nested content] B --> C["props.children"] C --> D[Text] C --> E[Elements] C --> F[Components] C --> G[A function] C --> H[An array of the above]
// The simplest possible wrapper
function Card({ children }) {
    return <div className="card">{children}</div>;
}

// Everything between the tags becomes Card's children
function App() {
    return (
        <Card>
            <h2>Card title</h2>
            <p>This paragraph and the heading above are Card's children.</p>
            <button>Click me</button>
        </Card>
    );
}

Why it matters: because Card renders {children} without inspecting it, the same Card can wrap a heading today and a chart tomorrow. That inversion — the parent decides the content, the wrapper decides the frame — is the engine of composition.

Children Can Be Anything

The children prop is not always "a list of elements." Depending on what you nest, it can be a string, a single element, an array of elements, null, or a function. Good wrapper components stay relaxed about which shape they get.

The children prop can hold text, a single element, multiple elements, or a function text "Hello" one element <p/> array [<li/>, <li/>] function (data) => JSX
One prop, many shapes. The React.Children helpers exist precisely to smooth over these differences.
function Container({ children }) {
    return <div className="container">{children}</div>;
}

// Text child
<Container>Hello world</Container>

// A single element child
<Container><h1>Title</h1></Container>

// Several children (React gives you an array internally)
<Container>
    <h1>Title</h1>
    <p>Paragraph</p>
</Container>

// Component children
<Container>
    <UserProfile user={currentUser} />
    <PostList posts={posts} />
</Container>

⚠️ Don't index into children directly

Because children is sometimes a single element and sometimes an array, children[0] or children.map(...) will crash for the single-child case. Use the React.Children helpers below, which normalize both.

React.Children Utilities

React ships a small namespace, React.Children, for working with the children prop safely — no matter which shape it arrived in.

React.Children.map — transform each child

Like Array.map, but it tolerates single children, null, and undefined without throwing, and it manages keys for you.

function List({ children }) {
    return (
        <ul className="list">
            {React.Children.map(children, (child) =>
                // Clone each child to add a shared className
                React.cloneElement(child, { className: 'list-item' })
            )}
        </ul>
    );
}

// Every <li> gets class="list-item" automatically
<List>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
</List>

React.Children.count — how many?

function Carousel({ children }) {
    const total = React.Children.count(children);
    const [active, setActive] = React.useState(0);

    return (
        <div className="carousel">
            {React.Children.map(children, (child, i) => (
                <div className={`slide ${i === active ? 'active' : ''}`}>
                    {child}
                </div>
            ))}
            <div className="controls">
                <button onClick={() => setActive((p) => (p === 0 ? total - 1 : p - 1))}>Prev</button>
                <span>{active + 1} / {total}</span>
                <button onClick={() => setActive((p) => (p === total - 1 ? 0 : p + 1))}>Next</button>
            </div>
        </div>
    );
}

React.Children.toArray — a real, keyed array

Turns children into a flat array with stable keys — handy when you need to filter, sort, or slice.

function FirstTwo({ children }) {
    const items = React.Children.toArray(children); // always an array
    return <div>{items.slice(0, 2)}</div>;          // render only the first two
}

React.Children.only — enforce a single child

function Tooltip({ children }) {
    // Throws a helpful error if more than one child is passed
    const child = React.Children.only(children);
    return React.cloneElement(child, { title: 'Extra info' });
}

💡 Use sparingly

These utilities are powerful but a little "magic." Most components should simply render {children} and stay out of the way. Reach for React.Children only when a component genuinely needs to inspect or transform what it was given — like a list that styles its items or a tab bar that wires up its tabs.

Injecting Props with cloneElement

You can't mutate a child element — React elements are immutable. Instead, React.cloneElement(element, newProps) returns a copy with extra props merged in. This lets a parent quietly wire behavior into children it didn't create.

function RadioGroup({ children, name, selectedValue, onChange }) {
    return (
        <div className="radio-group">
            {React.Children.map(children, (child) =>
                // Inject the shared name + wire up checked/onChange per option
                React.cloneElement(child, {
                    name,
                    checked: child.props.value === selectedValue,
                    onChange: () => onChange(child.props.value),
                })
            )}
        </div>
    );
}

function RadioButton({ children, ...props }) {
    return (
        <label>
            <input type="radio" {...props} /> {children}
        </label>
    );
}

function App() {
    const [selected, setSelected] = React.useState('a');
    return (
        <RadioGroup name="plan" selectedValue={selected} onChange={setSelected}>
            <RadioButton value="a">Plan A</RadioButton>
            <RadioButton value="b">Plan B</RadioButton>
        </RadioGroup>
    );
}

⚠️ cloneElement is a sharp tool

It couples the parent to the children's expected props and can be surprising to debug. For most "share data with descendants" needs, Context (shown next) is cleaner because children opt in explicitly. Save cloneElement for tight, well-documented component families like form controls.

Children as a Function

Children don't have to be markup — they can be a function the component calls. The component owns some state, invokes children(state), and the caller decides how to render it. This is the render-prop pattern wearing the children hat.

function DataProvider({ fetcher, children }) {
    const [state, setState] = React.useState({ data: null, loading: true, error: null });

    React.useEffect(() => {
        let active = true;
        fetcher()
            .then((data) => active && setState({ data, loading: false, error: null }))
            .catch((error) => active && setState({ data: null, loading: false, error }));
        return () => { active = false; }; // avoid setting state after unmount
    }, [fetcher]);

    // children is a function — call it with the current state
    return children(state);
}

// The consumer controls the UI for each state
<DataProvider fetcher={loadUsers}>
    {({ data, loading, error }) => {
        if (loading) return <Spinner />;
        if (error)   return <ErrorMessage error={error} />;
        return <UserTable users={data} />;
    }}
</DataProvider>

📖 Powerful, but a hook is often nicer

The children-as-a-function pattern was a favorite for sharing logic before hooks. Today the same DataProvider logic usually lives in a useData() custom hook, which avoids the extra nesting. Recognize this pattern in existing code and libraries — but reach for a custom hook first in new code.

Compound Components

The most elegant use of children is the compound component: a family of components that only make sense together and share state through Context, not prop drilling. An accordion is the classic example — the header and panel need to agree on which item is open.

import { createContext, useContext, useState } from 'react';

const AccordionContext = createContext(null);

// Parent owns the "which item is open" state and shares it via context
function Accordion({ children, defaultOpen = 0 }) {
    const [openIndex, setOpenIndex] = useState(defaultOpen);
    return (
        <AccordionContext.Provider value={{ openIndex, setOpenIndex }}>
            <div className="accordion">{children}</div>
        </AccordionContext.Provider>
    );
}

function AccordionItem({ index, title, children }) {
    const { openIndex, setOpenIndex } = useContext(AccordionContext);
    const isOpen = openIndex === index;
    return (
        <div className="accordion-item">
            <button
                className="accordion-header"
                aria-expanded={isOpen}
                onClick={() => setOpenIndex(isOpen ? -1 : index)}
            >
                {title} <span>{isOpen ? '▼' : '▶'}</span>
            </button>
            {isOpen && <div className="accordion-content">{children}</div>}
        </div>
    );
}

// Reads like plain markup; the shared state is invisible plumbing
function App() {
    return (
        <Accordion defaultOpen={0}>
            <AccordionItem index={0} title="Shipping">Ships in 2–3 days.</AccordionItem>
            <AccordionItem index={1} title="Returns">30-day free returns.</AccordionItem>
        </Accordion>
    );
}

✅ Why context beats cloneElement here

An older version of this pattern used cloneElement to push isOpen into each child. Context is cleaner: items pull what they need with useContext, so you can nest them any depth and reorder them freely without the parent re-wiring anything.

Practice & Quiz

🏋️ Exercise 1: An auto-styling List

Goal: Write a <List> that wraps its children in a <ul> and gives every child a className="list-item", safely handling one child or many.

function List({ children }) {
    // TODO: render a <ul> and clone each child to add className="list-item"
}

// <List><li>Only one</li></List>  should NOT crash
// <List><li>A</li><li>B</li></List>  should style both
💡 Hint

Use React.Children.map (not children.map) so the single-child case works, and React.cloneElement(child, { className: 'list-item' }) inside it.

✅ Solution
function List({ children }) {
    return (
        <ul className="list">
            {React.Children.map(children, (child) =>
                React.cloneElement(child, { className: 'list-item' })
            )}
        </ul>
    );
}

🏋️ Exercise 2: Toggle with function children

Goal: Build a <Toggle> that owns an on boolean and a toggle function, and passes both to its children function.

✅ Solution
function Toggle({ children }) {
    const [on, setOn] = React.useState(false);
    const toggle = () => setOn((prev) => !prev);
    return children({ on, toggle });
}

// Usage — the consumer decides the markup:
<Toggle>
    {({ on, toggle }) => (
        <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>
    )}
</Toggle>

🎯 Quick Quiz

Question 1: Why prefer React.Children.map over children.map?

Question 2: What does React.cloneElement(el, props) return?

Question 3: In a compound component like an accordion, how do the parts best share state?

Best Practices & Pitfalls

✅ Do

  • Default to just rendering {children} — keep wrappers dumb and flexible
  • Use React.Children helpers whenever you must inspect or map children
  • Prefer Context over cloneElement for sharing state with descendants
  • Reach for a custom hook before the children-as-a-function pattern in new code
  • Guard against null / conditional children so wrappers don't assume there's content

❌ Don't

  • Index into children directly (children[0]) — it breaks for a single child
  • Overuse cloneElement; it hides data flow and couples components tightly
  • Mutate a child element — clone it instead; elements are immutable
  • Build a compound component whose parts must appear in one rigid, undocumented order

⚠️ TypeScript note

When you add types later, type a normal wrapper's children as React.ReactNode, a single-element requirement as React.ReactElement, and a function child as (state: T) => React.ReactNode. Getting these right documents exactly what your component expects.

Summary

🎉 Key Takeaways

  • The children prop is whatever you nest between a component's tags
  • Children can be text, one element, an array, null, or a function — stay flexible
  • React.Children helpers safely map, count, arrayify, and enforce single children
  • cloneElement injects props into children — but Context is usually cleaner
  • Compound components share state through Context and read like plain markup

📚 Additional Resources

🚀 What's Next?

You've now seen behavior sharing peek through render props and compound components. Next we formalize one of the classic sharing tools: Higher-Order Components Basics — functions that take a component and return an enhanced one, and how they compare with today's custom hooks.

🎉 Great work!

The children prop is the quiet backbone of every reusable React component — and now it's yours to wield.