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 | 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:
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 errorWhen to Use React.ReactNode
I use React.ReactNode for most components. It matches how React actually works—any valid JSX child can be rendered.
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.
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:
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:
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:
// Flexible - accepts anythinginterface 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>// Strict - elements onlyinterface 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> // ✗ ErrorThe 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
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 assignableThe fix: wrap text in an element, or change the type to ReactNode.
Mistake 2: Thinking generics enforce element types
// This does NOT restrict to <button> elementsinterface ClickableProps { children: React.ReactElement<"button">;}
// Still passes type check with wrong element:<Clickable><div>Not a button</div></Clickable> // No errorThe generic parameter describes props, not the element tag.
Mistake 3: Forgetting fragments aren’t elements
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:
- The component manipulates the child element directly (cloning, reading props)
- The component expects exactly one element, not text or nothing
- I want to enforce that consumers wrap text in elements
// Default choice: ReactNode for flexibilityinterface CardProps { children: React.ReactNode;}
// Specific case: ReactElement when I need an elementinterface CloneWrapperProps { children: React.ReactElement; extraProps?: Record<string, unknown>;}
function CloneWrapper({ children, extraProps }: CloneWrapperProps) { // Safe to clone because we know it's an element return React.cloneElement(children, extraProps);}Key Takeaways
- React.ReactNode is the default choice—accepts everything JSX allows
- React.ReactElement is stricter—only JSX elements, no primitives
- TypeScript cannot enforce specific element types—no way to type “must be
<li>” - Use ReactElement when manipulating elements—cloning, reading props, single element required
- 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