Skip to content

React.ReactNode vs React.ReactElement: Which Type Should You Use for Children Props?

I was typing a React component’s children prop and hit a confusing choice. The React documentation showed two types: React.ReactNode and React.ReactElement. Both seemed to work. Which one should I pick? What’s the actual difference?

I tried using React.ReactElement for a component that renders text, and TypeScript threw errors. Then I switched to React.ReactNode and everything worked. But why? And when would I actually want the stricter type?

What Each Type Accepts

The React documentation defines these types clearly. React.ReactNode is “a union of all the possible types that can be passed as children in JSX.” React.ReactElement is “only JSX elements and not JavaScript primitives like strings or numbers.”

Here’s what that looks like in practice:

Type comparison table
| Type | Accepts |
|---------------------|--------------------------------------------------|
| React.ReactNode | JSX elements, strings, numbers, booleans, null, |
| | undefined, arrays, fragments, portals |
| React.ReactElement | Only JSX elements (<div>, <Component />) |

A quick way to visualize the difference:

What each type accepts
React.ReactNode (broad)
├── <div>Hello</div> ✓ JSX element
├── "Just text" ✓ String literal
├── {42} ✓ Number literal
├── {true} ✓ Boolean (renders nothing)
├── {null} ✓ Null (renders nothing)
├── {[1, 2, 3]} ✓ Array of children
└── <>Fragment</> ✓ Fragment
React.ReactElement (narrow)
├── <div>Hello</div> ✓ JSX element
├── "Just text" ✗ String - type error
├── {42} ✗ Number - type error
├── {true} ✗ Boolean - type error
├── {null} ✗ Null - type error
├── {[1, 2, 3]} ✗ Array - type error
└── <>Fragment</> ✗ Fragment - type error

When to Use React.ReactNode

I use React.ReactNode for most components. It matches how React actually works—any valid JSX child can be rendered.

Card.tsx
interface CardProps {
children: React.ReactNode;
}
function Card({ children }: CardProps) {
return <div className="card">{children}</div>;
}
// All these work:
<Card>Hello world</Card>
<Card>{42}</Card>
<Card><p>Paragraph</p></Card>
<Card>
<p>One</p>
<p>Two</p>
</Card>

The Card component doesn’t care what you pass as children. It wraps the content in a div. This flexibility is what I want most of the time.

When to Use React.ReactElement

I use React.ReactElement when my component needs to enforce that children must be a JSX element—not text, not numbers, not nothing.

ModalRenderer.tsx
interface ModalRendererProps {
title: string;
children: React.ReactElement;
}
function ModalRenderer({ title, children }: ModalRendererProps) {
// Component expects to manipulate a single element
return (
<div className="modal">
<h2>{title}</h2>
{children}
</div>
);
}
// Valid:
<ModalRenderer title="Settings"><SettingsPanel /></ModalRenderer>
// Type error:
<ModalRenderer title="Settings">Plain text</ModalRenderer>

The type error tells me I made a mistake. The component expects a real element, not a string.

But I noticed a problem. I wanted to type children as a specific element—only <li> elements for a list component. That doesn’t work.

The TypeScript Limitation

I tried this:

ListAttempt.tsx
interface ListProps {
children: React.ReactElement<"li">; // Hoping to restrict to <li>
}
function List({ children }: ListProps) {
return <ul>{children}</ul>;
}
// I expected this to error:
<List><div>Not an li</div></List> // But no error!

TypeScript didn’t catch the wrong element. The React documentation is clear: “You cannot use TypeScript to describe that the children are a certain type of JSX elements.”

The generic parameter on React.ReactElement<"li"> describes what props the element should have, not what HTML tag it is. TypeScript can’t enforce “must be an <li> element.”

If I need to validate element types, I have to do it at runtime:

ListRuntime.tsx
interface ListProps {
children: React.ReactNode;
}
function List({ children }: ListProps) {
// Runtime check
if (React.Children.count(children) > 0) {
React.Children.forEach(children, (child) => {
if (React.isValidElement(child) && child.type !== "li") {
console.warn("List children should be <li> elements");
}
});
}
return <ul>{children}</ul>;
}

A Practical Comparison

I built two versions of a wrapper component to see the difference clearly:

FlexibleWrapper.tsx
// Flexible - accepts anything
interface FlexibleWrapperProps {
children: React.ReactNode;
}
function FlexibleWrapper({ children }: FlexibleWrapperProps) {
return <section className="wrapper">{children}</section>;
}
// Usage - all valid:
<FlexibleWrapper>Hello</FlexibleWrapper>
<FlexibleWrapper><p>Content</p></FlexibleWrapper>
<FlexibleWrapper>{showContent && "Shown"}</FlexibleWrapper>
StrictWrapper.tsx
// Strict - elements only
interface StrictWrapperProps {
children: React.ReactElement;
}
function StrictWrapper({ children }: StrictWrapperProps) {
return <section className="wrapper">{children}</section>;
}
// Usage - only elements work:
<StrictWrapper><p>Content</p></StrictWrapper> // ✓
// Type errors:
<StrictWrapper>Hello</StrictWrapper> // ✗ Error
<StrictWrapper>{42}</StrictWrapper> // ✗ Error
<StrictWrapper>{showContent && "Shown"}</StrictWrapper> // ✗ Error

The last example is tricky. When showContent is false, the expression evaluates to false. That’s a boolean, not a ReactElement. TypeScript catches this.

Common Mistakes

I made these mistakes when learning the difference:

Mistake 1: Using ReactElement when I wanted text

TextError.tsx
interface LabelProps {
children: React.ReactElement; // Wrong choice
}
function Label({ children }: LabelProps) {
return <span className="label">{children}</span>;
}
// Error when I try to use it:
<Label>Username</Label> // Type error: string not assignable

The fix: wrap text in an element, or change the type to ReactNode.

Mistake 2: Thinking generics enforce element types

GenericLimitation.tsx
// This does NOT restrict to <button> elements
interface ClickableProps {
children: React.ReactElement<"button">;
}
// Still passes type check with wrong element:
<Clickable><div>Not a button</div></Clickable> // No error

The generic parameter describes props, not the element tag.

Mistake 3: Forgetting fragments aren’t elements

FragmentIssue.tsx
interface ContainerProps {
children: React.ReactElement;
}
// This fails:
<Container>
<>
<p>One</p>
<p>Two</p>
</>
</Container>

Fragments aren’t ReactElements. The type is too strict here.

What I Use Now

I default to React.ReactNode. It matches React’s mental model—children can be anything renderable.

I switch to React.ReactElement only when:

  1. The component manipulates the child element directly (cloning, reading props)
  2. The component expects exactly one element, not text or nothing
  3. I want to enforce that consumers wrap text in elements
PracticalExample.tsx
// Default choice: ReactNode for flexibility
interface CardProps {
children: React.ReactNode;
}
// Specific case: ReactElement when I need an element
interface CloneWrapperProps {
children: React.ReactElement;
extraProps?: Record&lt;string, unknown&gt;;
}
function CloneWrapper({ children, extraProps }: CloneWrapperProps) {
// Safe to clone because we know it's an element
return React.cloneElement(children, extraProps);
}

Key Takeaways

  1. React.ReactNode is the default choice—accepts everything JSX allows
  2. React.ReactElement is stricter—only JSX elements, no primitives
  3. TypeScript cannot enforce specific element types—no way to type “must be <li>
  4. Use ReactElement when manipulating elements—cloning, reading props, single element required
  5. Use ReactNode for flexibility—most components just render children

The choice isn’t about which is “better.” It’s about what contract your component needs. React.ReactNode says “I’ll render whatever you give me.” React.ReactElement says “I need a real JSX element to work with.”

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments