Question
I am migrating a React application to TypeScript and have run into confusion around return types in React components.
Until now, I have usually used JSX.Element as the return type for render functions. That works until a component returns null because it intentionally renders nothing. In that case, TypeScript reports an error because null is not assignable to JSX.Element.
After searching, I found suggestions to use ReactNode instead, since it includes null and other values React can render. But when I use ReactNode for functional components, TypeScript reports issues there as well. Then I found advice suggesting ReactElement for functional components, but that brings back the original problem because null is still not assignable.
So I want to understand three things clearly:
- What is the difference between
JSX.Element,ReactNode, andReactElement? - Why do class component render methods seem to return
ReactNode, while functional components are often typed as returningReactElement? - What is the correct way to type components that may return
null?
Short Answer
By the end of this page, you will understand what JSX.Element, ReactElement, and ReactNode represent in React with TypeScript, when each type should be used, and how to correctly type components that may return null. You will also see why React component return types are often misunderstood and what patterns are most practical in real codebases.
Concept
React can render more than just JSX tags. A component might render:
- a React element like
<div /> - plain text such as a string
- a number
- a fragment
- an array of renderable values
nullorfalseto render nothing
That is why React's type system has several related types.
ReactElement
ReactElement is the object created by JSX. When you write:
const el = <h1>Hello</h1>;
that value is a React element object.
In simple terms:
ReactElementdescribes a single React element object- it does not include
null - it does not include strings, numbers, or arrays directly
JSX.Element
JSX.Element is the type TypeScript uses for the result of a JSX expression.
In React projects, JSX.Element is usually very similar to ReactElement. In practice, people often treat them as almost interchangeable when talking about the result of JSX.
So this is usually fine:
const el: JSX.Element = <div />;
But like ReactElement, it represents a JSX-produced element value, not every possible renderable thing.
ReactNode
ReactNode is much broader. It represents anything React is allowed to render.
That includes values such as:
ReactElementstringnumbernullundefinedfalse- arrays of renderable values
- fragments and other renderable children
This makes ReactNode the best choice when you mean anything React can render, especially for children.
Why this matters
If you use a narrow type like JSX.Element, you may accidentally reject valid React output such as null.
If you use a broad type like ReactNode, you correctly model what React can render, but you should use it in the right places. The most common example is children:
type Props = {
children: React.ReactNode;
};
That allows callers to pass text, elements, fragments, or nothing.
Important practical point
In modern React + TypeScript, you often do not need to annotate a component's return type at all. TypeScript can infer it correctly:
function Message({ visible }: { visible: boolean }) {
if (!visible) return null;
return <p>Hello</p>;
}
This is often the simplest and most accurate solution.
Mental Model
Think of these three types like levels of a container system:
ReactElementis one finished Lego pieceJSX.Elementis the Lego piece produced when you write JSXReactNodeis anything you are allowed to put on the display table
So:
- a single Lego piece =
ReactElement - the result of building one piece with JSX =
JSX.Element - a full display that can include pieces, labels, numbers, empty space, or multiple pieces =
ReactNode
If a component may show nothing, that is still valid on the display table. That is why null fits ReactNode but not ReactElement or JSX.Element.
Syntax and Examples
Core syntax
const a: JSX.Element = <div>Hello</div>;
const b: React.ReactElement = <div>Hello</div>;
const c: React.ReactNode = <div>Hello</div>;
const d: React.ReactNode = "Hello";
const e: React.ReactNode = null;
Example: JSX.Element
const header: JSX.Element = <h1>Dashboard</h1>;
Use this when you specifically want a JSX-produced element.
Example: ReactElement
const button: React.ReactElement = <button>Save</button>;
This is similar in practice to JSX.Element for most React code.
Example: ReactNode
type CardProps = {
title: React.ReactNode;
};
function Card({ title }: CardProps) {
return <div>{title}</div>;
}
This is useful because title can be:
<Card title="Profile" />
<Card title={<strong>Profile</strong>} />
<Card title={null} />
Component that may return null
type Props = {
isVisible: boolean;
};
function Alert({ isVisible }: Props): React.ReactNode {
if (!isVisible) return null;
return <div>Warning</div>;
}
This type allows both valid outcomes: a rendered element or null.
Better: let TypeScript infer the return type
type Props = {
isVisible: boolean;
};
function Alert({ isVisible }: Props) {
if (!isVisible) return null;
return <div>Warning</div>;
}
This is often the cleanest approach.
children should usually be ReactNode
type LayoutProps = {
children: React.ReactNode;
};
function Layout({ children }: LayoutProps) {
return <main>{children}</main>;
}
This is the most common real-world use of ReactNode.
Step by Step Execution
Consider this component:
type Props = {
loggedIn: boolean;
};
function WelcomeMessage({ loggedIn }: Props) {
if (!loggedIn) {
return null;
}
return <h2>Welcome back!</h2>;
}
What happens step by step
1. The function receives props
If the component is used like this:
<WelcomeMessage loggedIn={false} />
then loggedIn is false.
2. The if condition runs
if (!loggedIn) {
return null;
}
Because loggedIn is false, the function returns null.
3. React renders nothing
Returning null is valid in React. It means the component intentionally produces no UI.
4. If loggedIn is true
<WelcomeMessage loggedIn={true} />
then the if block is skipped.
5. JSX is returned
return <h2>Welcome back!</h2>;
This JSX becomes a React element object.
Inferred return type
TypeScript sees both branches:
- one branch returns
null - one branch returns a JSX element
So it can infer a union-like return shape that matches valid React output.
That is why omitting the explicit return type often works better than forcing JSX.Element.
Real World Use Cases
1. Conditional UI
Many components only render under certain conditions:
function ErrorBanner({ error }: { error?: string }) {
if (!error) return null;
return <div>{error}</div>;
}
This is a common case where null matters.
2. children props
Reusable wrapper components often accept any renderable content:
type ModalProps = {
children: React.ReactNode;
};
That allows text, elements, fragments, and more.
3. Dynamic labels and content
UI libraries often allow labels to be either plain text or custom JSX:
type ButtonProps = {
label: React.ReactNode;
};
4. Lists and mixed output
A component might render arrays or mixed content:
const items: React.ReactNode = [<li key="1">A</li>, <li key="2">B</li>];
5. Layout and slots
Design systems often use ReactNode for header, footer, icon, and body slots:
type PanelProps = {
header?: React.ReactNode;
footer?: React.ReactNode;
};
This provides flexibility while staying type-safe.
Real Codebase Usage
In real React TypeScript projects, developers usually follow a few practical rules.
1. Do not annotate component return types unless needed
Most of the time, this is enough:
function UserBadge({ name }: { name: string }) {
return <span>{name}</span>;
}
And if the component may return nothing:
function UserBadge({ name }: { name?: string }) {
if (!name) return null;
return <span>{name}</span>;
}
TypeScript infers the correct return type.
2. Use ReactNode for children and renderable props
type Props = {
children: React.ReactNode;
emptyState?: React.ReactNode;
};
This is standard because those props can be text, elements, fragments, or null.
3. Use guard clauses for conditional rendering
function Profile({ user }: { user?: { name: string } }) {
if (!user) return null;
return <h1>{user.name}</h1>;
}
This keeps components easy to read.
4. Use narrower types only when you specifically need one element
Sometimes an API expects exactly one element object:
type Props = {
trigger: React.ReactElement;
};
That prevents passing raw text or arrays.
5. Be careful with React.FC
Some codebases use React.FC, but many avoid it unless there is a reason. It can add opinions about props and is not required for typing function components.
A simple function is often clearer:
function Button({ children }: { children: React.ReactNode }) {
return <button>{children}</button>;
}
Common Mistakes
Mistake 1: Forcing JSX.Element on components that return null
Broken example:
function Loader({ active }: { active: boolean }): JSX.Element {
if (!active) return null;
return <div>Loading...</div>;
}
Why it fails:
JSX.Elementdoes not includenull
Fix:
function Loader({ active }: { active: boolean }) {
if (!active) return null;
return <div>Loading...</div>;
}
Or:
function Loader({ active }: { active: boolean }): React.ReactNode {
if (!active) return null;
return <div>Loading...</div>;
}
Mistake 2: Using ReactElement for children
Broken example:
type Props = {
children: React.ReactElement;
};
Why it can be too strict:
- it rejects plain text
- it rejects
null - it rejects many valid child values
Better:
type Props = {
children: React.ReactNode;
};
Mistake 3: Assuming JSX.Element and ReactNode mean the same thing
They do not.
JSX.Elementis a specific JSX result typeReactNodeis any renderable value
Mistake 4: Over-annotating everything
Beginners often add explicit types everywhere, even when inference is better.
Instead of this:
function Greeting(): React.ReactElement {
return <h1>Hello</h1>;
}
this is often enough:
function Greeting() {
return <h1>Hello</h1>;
}
Mistake 5: Thinking false is invalid in rendering
React allows this:
const content: React.ReactNode = false;
It renders nothing, similar to null in many conditional cases.
Comparisons
| Type | What it represents | Includes null? | Includes strings/numbers? | Best use |
|---|---|---|---|---|
JSX.Element | Result of a JSX expression | No | No | Typing a specific JSX value |
React.ReactElement | A React element object | No | No | APIs that require exactly one React element |
React.ReactNode | Anything React can render | Yes | Yes | children, renderable props, nullable output |
JSX.Element vs ReactElement
In many React TypeScript projects, these are very close in day-to-day use. Both represent element-shaped values, not every renderable value.
ReactElement vs ReactNode
ReactElement is narrower.
const a: React.ReactElement = <div />; // valid
const b: React.ReactNode = <div />; // valid
const c: React.ReactNode = "text"; // valid
const d: React.ReactElement = "text"; // invalid
Should function components return ReactElement?
Historically, you may see examples or older typings that suggest this. In practice today:
- if you explicitly annotate and the component may return
null,ReactElementis too narrow - inference is usually better
ReactNodeis acceptable when you want to express any valid render output
Class component render() vs function component return types
render() is commonly understood as returning anything React can render, while many function component examples were written with narrower element types. The confusion comes more from typing conventions and historical patterns than from a meaningful React behavior difference.
Cheat Sheet
Quick rules
- Use
React.ReactNodeforchildren - Use
React.ReactNodefor props that can accept anything renderable - Use
React.ReactElementwhen you need exactly one React element - Use
JSX.Elementfor a JSX value, not for all possible render output - If a component may return
null, do not forceJSX.Element - Usually, let TypeScript infer the component return type
Common patterns
type Props = {
children: React.ReactNode;
};
function Component() {
return <div />;
}
function MaybeVisible({ show }: { show: boolean }) {
if (!show) return null;
return <div>Visible</div>;
}
type Props = {
icon: React.ReactElement;
};
What includes null?
ReactNode: yesReactElement: noJSX.Element: no
Best default advice
If you are typing React components in TypeScript:
- annotate props explicitly
- usually skip explicit return type annotations
- use
ReactNodeforchildrenand flexible renderable props
FAQ
What is the difference between ReactNode and ReactElement?
ReactElement is one React element object. ReactNode is anything React can render, including elements, strings, numbers, arrays, and null.
Is JSX.Element the same as ReactElement?
In many React TypeScript setups, they are very similar in practice, but they are not conceptually identical. Both are narrower than ReactNode.
Should I use ReactNode as a component return type?
You can, especially if the component may return null. But in many cases, it is simpler to let TypeScript infer the return type.
Why does returning null cause an error with JSX.Element?
Because JSX.Element represents a JSX element value, and null is not part of that type.
What type should children be in React TypeScript?
Usually React.ReactNode, because children can be text, elements, fragments, arrays, or nothing.
Should I use React.FC for function components?
You can, but it is not required. Many developers prefer plain functions with explicitly typed props.
When should I use ReactElement specifically?
Use it when you need exactly one React element, such as a prop that must receive a single element and not plain text or null.
Mini Project
Description
Build a small notification panel component that demonstrates the difference between values that must be a single React element and values that can be any renderable content. The project also shows how a component can safely return null when there is nothing to display.
Goal
Create a React TypeScript component set that uses ReactNode for flexible content, ReactElement for a required icon element, and returns null when no notification should be shown.
Requirements
- Create a
Notificationcomponent that accepts amessageprop typed asReact.ReactNode. - Add an
iconprop that must be typed asReact.ReactElement. - Add a
visibleboolean prop and returnnullwhen it isfalse. - Render the component with both plain text and JSX-based messages.
- Show one valid
iconexample using JSX.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.