Je migre actuellement une application React vers TypeScript. Jusqu'à présent, cela fonctionne plutôt bien, mais j'ai un problème avec les types de retour de mes fonctions render
respectivement mes composants de fonction.
Jusqu'à présent, j'avais toujours utilisé JSX.Element
comme type de retour, maintenant cela ne fonctionne plus si un composant décide de pas rendre quoi que ce soit, c'est-à-dire retourner null
, puisque null
n'est pas une valeur valide pour JSX.Element
. Ce fut le début de mon voyage, car maintenant j'ai cherché sur le Web et j'ai trouvé que vous devriez utiliser ReactNode
à la place, ce qui inclut également null
et aussi quelques autres choses qui peuvent arriver. Cela semblait être le meilleur pari.
Cependant, maintenant lors de la création d'un composant fonction, TypeScript se plaint du type ReactNode
. Encore une fois, après quelques recherches, j'ai trouvé que pour les composants de fonction, vous devriez utiliser ReactElement
à la place. Cependant, si je le fais, le problème de compatibilité a disparu, mais maintenant TypeScript se plaint à nouveau que null
n'est pas une valeur valide.
Donc, pour faire court, j'ai trois questions:
JSX.Element
, ReactNode
et ReactElement
?render
des composants de classe renvoient ReactNode
, mais les composants de fonction renvoient ReactElement
?null
?Quelle est la différence entre JSX.Element, ReactNode et ReactElement?
Un ReactElement est un objet avec un type et des accessoires.
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
type: T;
props: P;
key: Key | null;
}
Un ReactNode est un ReactElement, un ReactFragment, une chaîne, un nombre ou un tableau de ReactNodes, ou null, ou non défini, ou un booléen:
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 est un ReactElement, le type générique pour props et type étant any. Il existe, car diverses bibliothèques peuvent implémenter JSX à leur manière, donc JSX est un espace de noms global qui est ensuite défini par la bibliothèque, React le définit comme ceci:
declare global {
namespace JSX {
interface Element extends React.ReactElement<any, any> { }
}
}
Par exemple:
<p> // <- ReactElement = JSX.Element
<Custom> // <- ReactElement = JSX.Element
{true && "test"} // <- ReactNode
</Custom>
</p>
Pourquoi les méthodes de rendu des composants de classe renvoient ReactNode, mais les composants de fonction renvoient ReactElement?
En effet, ils retournent des choses différentes. Component
s retour:
render(): ReactNode;
Et les fonctions sont des "composants sans état":
interface StatelessComponent<P = {}> {
(props: P & { children?: ReactNode }, context?: any): ReactElement | null;
// ... doesn't matter
}
Comme vous pouvez le voir, les composants fonctionnels peuvent avoir ReactNodes comme enfants, mais ils doivent renvoyer un ReactElement ou null. Si vous y pensez, c'est en fait la même chose qu'avec les composants: ils sont eux-mêmes ReactElement
s, mais ils peuvent avoir ReactNode
s comme des enfants.
Comment résoudre ce problème par rapport à null?
Tapez-le comme ReactElement | null
tout comme réagit. Ou laissez TypeScript déduire le type.
1.) Quelle est la différence entre JSX.Element, ReactNode et ReactElement?
ReactElement et JSX.Element sont le résultat de l'invocation React.createElement
directement ou via la transpilation JSX. C'est un objet avec type
, props
et key
. JSX.Element
est ReactElement
, dont props
et type
ont le type any
, ils sont donc plus ou moins le même.
const jsx = <div>hello</div>
const ele = React.createElement("div", null, "hello");
ReactNode est utilisé comme type de retour pour render()
dans les composants de classe. C'est également le type par défaut pour l'attribut children
avec PropsWithChildren
.
const Comp: FunctionComponent = props => <div>{props.children}</div>
// children?: React.ReactNode
Cela semble plus compliqué dans les déclarations de type React , mais est équivalent à:
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...)
Vous pouvez affecter presque tout à ReactNode
, cela ne vous aidera pas avec les types forts !
2.) Pourquoi les méthodes de rendu des composants de classe renvoient ReactNode, mais les composants de fonction renvoient ReactElement?
tl; dr: Il s'agit d'une incompatibilité de type TS actuelle non liée au core React, voir le suivi problème principal .
Avec le côté pur React/JS, render()
dans les composants de classe supporte les mêmes types de retour qu'un composant de fonction, il y a pas de différences . En ce qui concerne TS, les différents types ne sont actuellement qu'une incohérence de type toujours conservée pour des raisons historiques et le besoin de rétrocompatibilité.
Le problème mentionné veut également prendre en compte le changement qu'un composant rend le type de retour - fonction et classe - entre-temps prend en charge plus de types que JSX.Element
. Idéalement, un type de retour valide ressemblerait davantage à ceci:
type ComponentReturnType = ReactElement | Array<ReactElement> | string | number | boolean
| null // Note: undefined is invalid!
Mais actuellement dans TS, un composant de classe est fixé pour renvoyer ReactNode
avec render()
, qui est plus permissif que React permet. Le React.FunctionComponent
le type de retour est JSX.Element | null
, dont les types sont plus restrictifs que React prend en charge.
3.) Comment résoudre ce problème par rapport à null?
Quelques options et orientation pour les types de retour de composant de fonction:
Omettre complètement l'annotation de type de retour et s'appuyer sur l'inférence de type
const MyComp = () => condition ? <div>Hello</div> : null // returns: JSX.Element| null
render
explicites aussi étroits que possible (bonne pratique pour children
également)ReactNode
, donc je voudrais éviter ça complètementReactElement
(importation de module "react"
) À JSX.Element
( portée globale)ReactChild
au lieu de ReactElement
, si vous devez retourner string | number
const MyCompFragment: FunctionComponent = () => <>"huhu"</>
const MyCompCast: FunctionComponent = () => "huhu" as any
// or: as unknown as JSX.Element | null
React.FunctionComponent
Et utilisez simplement des fonctions simples - le type vientavecbeaucoupcaprices . C'est aussi pourquoi créer React App abandonné à partir de son modèle