27.4 Children Prop: React.ReactNode, React.ReactElement, and PropsWithChildren
Alright, let’s talk about the children prop. You’ve seen it. You’ve used it. You’ve probably typed it as any or just ignored it entirely to make the red squiggly line in your editor go away. We’ve all been there, but it’s time to level up.
In React, children is a special prop, passed implicitly. It’s the content you put between your component’s opening and closing tags. Think of <Button>Click Me!</Button> – "Click Me!" is the children prop. Simple, right? The complexity comes when you, the brilliant developer, want to type this in TypeScript. What the heck goes in that interface?
The Contenders: ReactNode, ReactElement, and the Pretender
TypeScript gives you a few options, each with a different level of specificity and control. Choosing the right one is the difference between a flexible, robust component and a frustrating, brittle one.
First up, the kitchen sink: React.ReactNode. This is the most inclusive type. It accepts absolutely anything that React can render: strings, numbers, JSX elements, arrays of those things, fragments, null, undefined, booleans (which are ignored)… you name it. It’s the “I don’t care, just render it” type.
interface CardProps {
title: string;
children: React.ReactNode; // Anything goes!
}
const Card = ({ title, children }: CardProps) => {
return (
<div className="card">
<h2>{title}</h2>
<div>{children}</div> {/* Could be anything */}
</div>
);
};
// All of these are perfectly valid:
<Card title="Blog Post">
<p>Some text</p>
<p>More text</p>
</Card>
<Card title="Just a String">
Hello, World!
</Card>
<Card title="Empty" /> // children is undefined
Use this when your component is a pure layout or container component that just needs to slap its children into the DOM somewhere. This covers about 95% of your use cases. It’s the correct default choice.
Now, the bouncer: React.ReactElement. This is far more restrictive. It only accepts a single JSX element. No strings, no numbers, no arrays, no nothing. Just one element.
interface ModalProps {
isOpen: boolean;
children: React.ReactElement; // Exactly one element, please.
}
const Modal = ({ isOpen, children }: ModalProps) => {
if (!isOpen) return null;
return <div className="modal">{children}</div>;
};
// This works:
<Modal isOpen={true}>
<div>Some Content</div>
</Modal>
// These will cause type errors:
// <Modal isOpen={true}>Hello World</Modal> // String? Nope.
// <Modal isOpen={true}>
// <p>Paragraph</p>
// <button>Click</button> // Two elements? Nope.
// </Modal>
You’d use this when you need to enforce a strict contract, perhaps because you’re doing something dangerous like cloning the element and injecting props into it with React.cloneElement. It’s powerful but often overkill. Don’t reach for it unless you have a specific, good reason. Needing exactly one child is usually a sign you should just design your component API better.
The Convenience Helper: PropsWithChildren
Then there’s React.PropsWithChildren. This is a TypeScript type helper, not a specific type for children itself. It’s a generic type that takes your props interface and slaps a children?: React.ReactNode property onto it.
import { PropsWithChildren } from 'react';
interface MyComponentProps {
title: string;
// I don't have to manually declare children here
}
// This is equivalent to { title: string; children?: React.ReactNode }
const MyComponent: React.FC<PropsWithChildren<MyComponentProps>> = ({ title, children }) => {
return <div>{title}{children}</div>;
};
Here’s my hot take: I don’t use it anymore, and you probably shouldn’t either. The React.FC type (Function Component) and PropsWithChildren were the old way. The modern, recommended approach is to just explicitly include children?: React.ReactNode in your interface if your component accepts them. It’s more explicit, clearer to read, and gives you finer-grained control. The helper feels like unnecessary abstraction.
The Gotchas and Pitfalls
Boolean Landmines: Remember,
trueandfalseare validReactNodevalues, but React intentionally doesn’t render them. This is a common source of confusion when a condition returns a boolean and you try to use it aschildren. Your component will render seemingly nothing. Always make sure your expressions resolve to something renderable.The “No Children” Component: Sometimes you don’t want your component to accept children. A good example is an
<Input />field or an<Img />component. In these cases, the most correct thing to do is to omit thechildrenprop from your interface entirely. This will cause a TypeScript error if someone tries to pass them, which is exactly what you want. Don’t usenever; just don’t define it.
The rule of thumb is simple: Start with React.ReactNode. It’s the correct choice for almost everything. Only move to the more restrictive React.ReactElement when you have a specific, technical requirement to do so. And don’t bother with the helpers; be explicit. Your future self, trying to decipher your own code at 2 AM, will thank you for it.