742

I am currently migrating a React application to TypeScript. So far, this works pretty well, but I have a problem with the return types of my render functions, specifically in my functional components.

I have always used JSX.Element as the return type, now this doesn't work any more if a component decides to not render anything, i.e. returns null, since null is not a valid value for JSX.Element. This was the beginning of my journey. I searched the web and found that you should use ReactNode instead, which includes null and a few other things that can happen.

However, when creating a functional component, TypeScript complains about the ReactNode type. Again, after some searching I found, that for functional components you should use ReactElement instead. However, if I do so, the compatibility issue is gone, but now TypeScript again complains about null not being a valid value.

To cut a long story short, I have three questions:

  1. What is the difference between JSX.Element, ReactNode and ReactElement?
  2. Why do the render methods of class components return ReactNode, but functional components return ReactElement?
  3. How do I solve this with respect to null?
Dave Mackey
  • 4,306
  • 21
  • 78
  • 136
Golo Roden
  • 140,679
  • 96
  • 298
  • 425
  • 3
    I'm surprised this would come up, since usually you don't need to specify the return type with components. What type signature are you doing for the components? Should look something like `class Example extends Component {` for classes, and `const Example: FunctionComponent = (props) => {` for function components (where `ExampleProps` is an interface for the expected props). And then these types have enough information that the return type can be inferred. – Nicholas Tower Sep 26 '19 at 19:36
  • 3
    The types are defined [here](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/eafef8bd049017b3998939de2edbab5d8a96423b/types/react/v15/index.d.ts#L187) – Jonas Wilms Sep 26 '19 at 19:38
  • 22
    @NicholasTower Our linting rules enforce explicitly providing the return type, and that's why this comes up (which IMHO is a good thing, because you think way more about what you do, which helps understanding, than if you just let the compiler infer everything). – Golo Roden Sep 26 '19 at 19:42
  • Fair enough, i didn't think of linting rules. – Nicholas Tower Sep 26 '19 at 19:43
  • @JonasWilms Thanks for the link, but I don't think that this answers my questions. – Golo Roden Sep 26 '19 at 19:43

5 Answers5

748

What is the difference between JSX.Element, ReactNode and ReactElement?

A ReactElement is an object with a type and props.

 type Key = string | number

 interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
    type: T;
    props: P;
    key: Key | null;
}

A ReactNode is a ReactElement, a ReactFragment, a string, a number or an array of ReactNodes, or null, or undefined, or a boolean:

type ReactText = string | number;
type ReactChild = ReactElement | ReactText;

interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;

type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;

JSX.Element is a ReactElement, with the generic type for props and type being any. It exists, as various libraries can implement JSX in their own way, therefore JSX is a global namespace that then gets set by the library, React sets it like this:

declare global {
  namespace JSX {
    interface Element extends React.ReactElement<any, any> { }
  }
}

By example:

 <p> // <- ReactElement = JSX.Element
   <Custom> // <- ReactElement = JSX.Element
     {true && "test"} // <- ReactNode
  </Custom>
 </p>

Why do the render methods of class components return ReactNode, but function components return ReactElement?

Indeed, they do return different things. Components return:

 render(): ReactNode;

And functions are "stateless components":

 interface StatelessComponent<P = {}> {
    (props: P & { children?: ReactNode }, context?: any): ReactElement | null;
    // ... doesn't matter
}

This is actually due to historical reasons.

How do I solve this with respect to null?

Type it as ReactElement | null just as react does. Or let Typescript infer the type.

source for the types

rMonteiro
  • 1,371
  • 1
  • 14
  • 37
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • 1
    Thanks for the detailed answer! This perfectly answers question 1, but still leaves questions 2 and 3 unanswered. Can you provide some guidance on them, too, please? – Golo Roden Sep 26 '19 at 19:58
  • 2
    @goloRoden sure, I'm just a bit tired right now and it takes some time to scroll through the types on a mobile ... ;) – Jonas Wilms Sep 26 '19 at 20:10
  • Hi @JonasWilms. Regarding "They don't. ReactComponent is defined as: render(): JSX.Element | null | false;", where are you seeing that? It looks like it returns ReactNode to me (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/bdfdaf7fef18e267ed2df2e37e4bf81y685706eb2/types/react/index.d.ts#L443 ) Also minor typo, I think `ReactComponent` should be `React.Component` or `Component`. – Mark Doliner Dec 19 '19 at 05:23
  • @MarkDoliner Weird, I could swear that I copied that type out of the file ... Whatever, you're totally right, I'll edit – Jonas Wilms Dec 19 '19 at 13:05
  • 1
    Guys is there any official documentation of the react && typescript somewhere on the web? – Goran_Ilic_Ilke Aug 17 '20 at 17:50
93

1.) What is the difference between JSX.Element, ReactNode and ReactElement?

ReactElement and JSX.Element are the result of invoking React.createElement directly or via JSX transpilation. It is an object with type, props and key. JSX.Element is ReactElement, whose props and type have type any, so they are more or less the same.

const jsx = <div>hello</div>
const ele = React.createElement("div", null, "hello");

ReactNode is used as return type for render() in class components. It also is the default type for children attribute with PropsWithChildren.

const Comp: FunctionComponent = props => <div>{props.children}</div> 
// children?: React.ReactNode

It looks more complicated in the React type declarations, but is equivalent to:

type ReactNode = {} | null | undefined;
// super type `{}` has absorbed *all* other types, which are sub types of `{}`
// so it is a very "broad" type (I don't want to say useless...)

You can assign almost everything to ReactNode. I usually would prefer stronger types, but there might be some valid cases to use it.


2.) Why do the render methods of class components return ReactNode, but function components return ReactElement?

tl;dr: It is a current TS type incompatibility not related to core React.

  • TS class component: returns ReactNode with render(), more permissive than React/JS

  • TS function component: returns JSX.Element | null, more restrictive than React/JS

In principle, render() in React/JS class components supports the same return types as a function component. With regard to TS, the different types are a type inconsistency still kept due to historical reasons and the need for backwards-compatibility.

Ideally a valid return type would probably look more like this:

type ComponentReturnType = ReactElement | Array<ComponentReturnType> | string | number 
  | boolean | null // Note: undefined is invalid

3.) How do I solve this with respect to null?

Some options:
// Use type inference; inferred return type is `JSX.Element | null`
const MyComp1 = ({ condition }: { condition: boolean }) =>
    condition ? <div>Hello</div> : null

// Use explicit function return types; Add `null`, if needed
const MyComp2 = (): JSX.Element => <div>Hello</div>; 
const MyComp3 = (): React.ReactElement => <div>Hello</div>;  
// Option 3 is equivalent to 2 + we don't need to use a global (JSX namespace)

// Use built-in `FunctionComponent` or `FC` type
const MyComp4: React.FC<MyProps> = () => <div>Hello</div>;

Note: Avoiding React.FC won't save you from the JSX.Element | null return type restriction.

Create React App recently dropped React.FC from its template, as it has some quirks like an implicit {children?: ReactNode} type definition. So using React.FC sparingly might be preferable.

In edge cases, you can add a type assertion or Fragments as workaround:
const MyCompFragment: FunctionComponent = () => <>"Hello"</>
const MyCompCast: FunctionComponent = () => "Hello" as any 
// alternative to `as any`: `as unknown as JSX.Element | null`
Community
  • 1
  • 1
ford04
  • 66,267
  • 20
  • 199
  • 171
  • 5
    This Answer is great. However, I use React 18 in 2022. The `ReactFragment` is `Iterable` now, there is no type `{}` anymore. – Hao-Jung Hsieh Jun 22 '22 at 07:48
52

ReactElement is the type for elements in React, either created via JSX or React.createElement.

const a = <div/> // this is a ReactElement

ReactNode is wider, it can be text, number, boolean, null, undefined, a portal, a ReactElement, or an array of ReactNodes. It represents anything that React can render.

const a = (
  <div>
     hello {[1, "world", true]} // this is a ReactNode
  </div>
)

JSX.Element is an internal hook for Typescript. It is set equal to ReactElement to tell Typescript that every JSX expressions should be typed as ReactElements. But if we'd use Preact, or other technologies using JSX it would be set to something else.

Functional components return ReactElement | null, so it cannot return a bare string or an array of ReactElements. It is a known limitation. The workaround is to use Fragments :

const Foo = () => {
  return <>hello world!</>  // this works
}

Class components' render function return ReactNode, so there shouldn't be any problem.

15

https://github.com/typescript-cheatsheets/react#useful-react-prop-type-examples

export declare interface AppProps {
  children1: JSX.Element; // bad, doesnt account for arrays
  children2: JSX.Element | JSX.Element[]; // meh, doesn't accept strings
  children3: React.ReactChildren; // despite the name, not at all an appropriate type; it is a utility
  children4: React.ReactChild[]; // better, accepts array children
  children: React.ReactNode; // best, accepts everything (see edge case below)
  functionChildren: (name: string) => React.ReactNode; // recommended function as a child render prop type
  style?: React.CSSProperties; // to pass through style props
  onChange?: React.FormEventHandler<HTMLInputElement>; // form events! the generic parameter is the type of event.target
  //  more info: https://react-typescript-cheatsheet.netlify.app/docs/advanced/patterns_by_usecase/#wrappingmirroring
  props: Props & React.ComponentPropsWithoutRef<"button">; // to impersonate all the props of a button element and explicitly not forwarding its ref
  props2: Props & React.ComponentPropsWithRef<MyButtonWithForwardRef>; // to impersonate all the props of MyButtonForwardedRef and explicitly forwarding its ref
}
Hossam Mourad
  • 4,369
  • 4
  • 26
  • 22
0

Let's understand JSX.Element vs React.ReactElement vs React.ReactNode step by step:

JSX.Element and React.ReactElement

JSX.Element and React.ReactElement are functionally the same type. They can be used interchangeably.

Consider below example:

const element = <div className="greeting">Hello, world!</div>;

When the above JSX code runs, it doesn't directly create something you can see on the screen. Instead, it creates a JavaScript object that describes that piece of UI. This object is like a blueprint or a recipe. This is how this blueprint or recipe object will look like:

const element = {
  type: 'div',
  props: {
    className: 'greeting',
    children: 'Hello, world!'
  }
};
  1. type: This tells React what type of element we want to create. In this case, it's a div.
  2. props: This is an object that holds all the properties (or "props") that we've passed to our element. Here, we've passed a className prop with the value 'greeting'.
  3. children: This is a special prop that represents the content inside our element. In this case, it's the string 'Hello, world!'.

This object is what React uses to understand how to build and update the actual UI on the screen. When React sees this object, it knows to create a div element, give it a class of 'greeting', and put the text 'Hello, world!' inside it.

In a nutshell, we say A ReactElement is an object with a type and props.

So what is the difference between JSX.Element and React.ReactElement?

JSX.Element is a TypeScript type for JSX expressions, and React.ReactElement is a React type for React elements.

Is this correct?

const Component = ({
  children,
}: {
  children: JSX.Element;
}) => {
  return <div>{children}</div>;
};


// Usage

<Component>hello world</Component>

In above stated example, TypeScript isn't happy because we declared the type of children to be JSX.Element, however, we are passing a string ("hello world") which is not a JSX.Element. Hence, we need a different type for children that can accept string, number, null, ReactElement ....

This is where React.ReactNode shines.

React.ReactNode

As per below definition of React.ReactNode.

React.ReactNode is a ReactElement, string, number, ReactFragment, ReactPortal, boolean, null & undefined.

declare namespace React {
  type ReactNode =
    | ReactElement
    | string
    | number
    | ReactFragment
    | ReactPortal
    | boolean
    | null
    | undefined;
}

Example:

const Component = ({
  children,
}: {
  children: React.ReactNode
}) => {
  return <div>{children}</div>;
};


// Usage
<Component> Hello World </Component>

Now TypeScript is happy!!

Happy coding!

Basir Payenda
  • 361
  • 3
  • 6