Reputation: 150892
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:
JSX.Element
, ReactNode
, and ReactElement
?render
methods of class components return ReactNode
but functional components return ReactElement
?null
?Upvotes: 882
Views: 510564
Reputation: 138497
What is the difference between
JSX.Element
,ReactNode
, andReactElement
?
A ReactElement
is an object with type
, props
, and key
properties:
interface ReactElement<
P = any,
T extends
| string
| JSXElementConstructor<any> = string
| JSXElementConstructor<any>,
> {
type: T;
props: P;
key: string | null;
}
A JSX.Element
is a ReactElement<any, any>
. It exists as various libraries can implement JSX
in their own way:
declare global {
// …
namespace JSX {
// …
interface Element extends React.ReactElement<any, any> {}
// …
}
// …
}
A ReactPortal
is a ReactElement
with a children
property:
interface ReactPortal extends ReactElement {
children: ReactNode;
}
A ReactNode
is a ReactElement
, string
, number
, Iterable<ReactNode>
, ReactPortal
, boolean
, null
, or undefined
:
type ReactNode =
| ReactElement
| string
| number
| Iterable<ReactNode>
| ReactPortal
| boolean
| null
| undefined;
Example:
<div> // <- ReactElement
<Component> // <- ReactElement
{condition && 'text'} // <- ReactNode
</Component>
</div>
Why do the render methods of class components return ReactNode, but function components return ReactElement?
This is due to historical reasons.
A Component.render
returns a ReactNode
:
class Component<P, S> {
// …
render(): ReactNode;
// …
}
A FunctionComponent
returns a ReactElement<any, any> | null
:
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P> | undefined;
contextTypes?: ValidationMap<any> | undefined;
defaultProps?: Partial<P> | undefined;
displayName?: string | undefined;
}
How do I solve this with respect to null?
Type it as ReactElement | null
just as React does. Or let TypeScript infer the type.
Upvotes: 895
Reputation: 605
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!'
}
};
div
.className
prop with the value 'greeting'
.'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.
JSX.Element
and React.ReactElement
?JSX.Element
is a TypeScript type for JSX expressions, and React.ReactElement
is a React type for React elements.
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.
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!
Upvotes: 15
Reputation: 2842
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.
Upvotes: 74
Reputation: 4659
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
}
Upvotes: 17
Reputation: 74740
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
Some options:3.) How do I solve this with respect to null?
// 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.
const MyCompFragment: FunctionComponent = () => <>"Hello"</>
const MyCompCast: FunctionComponent = () => "Hello" as any
// alternative to `as any`: `as unknown as JSX.Element | null`
Upvotes: 111