Lesson 42 of 80
children looks like special JSX syntax. It's actually just a prop named 'children' — and once you see that, cloneElement and React.Children stop feeling like magic.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Writing <Card>hello</Card> looks like dedicated syntax, but it desugars to React.createElement(Card, null, "hello") — the third argument becomes props.children. <Card> and <Card children="hello"> produce the exact same element. children is a naming convention with first-class support in JSX's syntax, not a separate language feature. That's why you can read it, ignore it, wrap it, count it, or replace it entirely — it behaves like any other prop because it is one.
Because children can be anything a prop can be — a string, an array of elements, a single element, null, even a function (the render-props pattern uses exactly that) — components sometimes need to work with it structurally instead of just dropping it in with {children}.
import { Children, cloneElement, isValidElement } from "react";
function ButtonGroup({ children, activeIndex }) {
const items = Children.toArray(children); // flattens, keys, filters null/undefined
return (
<div className="button-group">
{Children.map(items, (child, index) => {
if (!isValidElement(child)) return child;
return cloneElement(child, {
isActive: index === activeIndex,
});
})}
</div>
);
}
// usage — each <Button> never explicitly receives isActive from the caller
<ButtonGroup activeIndex={1}>
<Button>Overview</Button>
<Button>Details</Button>
<Button>History</Button>
</ButtonGroup>;
Children.map iterates safely over whatever shape children came in as (a single child isn't an array, but Children.map/Children.count handle that without you checking). cloneElement takes an existing element and returns a new one with extra props merged in — here, injecting isActive into each Button without ButtonGroup's caller ever passing it explicitly.
React.Children utilities are rarely necessary today. They exist for a pre-Hooks era where injecting shared state into a fixed set of children was the main option. Context plus a custom hook now covers almost every case that used to reach for Children.map/cloneElement, with far less fragility.cloneElement breaks the moment children isn't literally a list of the elements you expect. If a caller wraps a Button in a <div>, or passes a plain string, isValidElement returns false and your injected prop silently never arrives — there's no compile-time guarantee about what children contains.cloneElement(child, { isActive }) only wins if the child doesn't already have its own isActive prop set explicitly — cloneElement merges props, and props passed directly to the clone call take precedence over the child's existing props only for the keys you actually pass, not the other way around, which surprises people expecting a hard override.cloneElement operates on the same lightweight element objects that React.createElement produces — the same objects the reconciliation lesson describes React walking and diffing. Cloning one with new props doesn't mutate anything; it produces a brand-new object, consistent with JSX's underlying immutability everywhere else.