Kiedy używać JSX.Element vs ReactNode vs ReactElement?

168

Obecnie przeprowadzam migrację aplikacji React do TypeScript. Jak dotąd działa to całkiem nieźle, ale mam problem ze zwracanymi typami moich renderfunkcji, odpowiednio, z komponentami funkcji.

Do tej pory zawsze używałem JSX.Elementjako typu zwracanego, teraz to już nie działa, jeśli komponent zdecyduje się nie renderować niczego, tj. Zwrócić null, ponieważ nullnie ma prawidłowej wartości dla JSX.Element. To był początek mojej podróży, ponieważ teraz przeszukałem sieć i stwierdziłem, że powinieneś ReactNodezamiast tego użyć , co obejmuje nullrównież, a także kilka innych rzeczy, które mogą się zdarzyć. To wydawało się być lepszym rozwiązaniem.

Jednak teraz podczas tworzenia składnika funkcji TypeScript narzeka na ReactNodetyp. Ponownie, po kilku poszukiwaniach stwierdziłem, że dla komponentów funkcji powinieneś użyć ReactElementzamiast tego. Jednak jeśli to zrobię, problem ze zgodnością zniknie, ale teraz TypeScript ponownie narzeka, że nullnie jest prawidłową wartością.

Krótko mówiąc, mam trzy pytania:

  1. Jaka jest różnica między JSX.Element, ReactNodei ReactElement?
  2. Dlaczego renderzwracają się metody składników klas ReactNode, a składniki funkcji zwracają ReactElement?
  3. Jak rozwiązać ten problem w odniesieniu do null?
Golo Roden
źródło
1
Dziwię się, że tak się stanie, ponieważ zwykle nie trzeba określać typu zwracanego z komponentami. Jaki typ podpisu robisz dla komponentów? Powinien wyglądać podobnie class Example extends Component<ExampleProps> {dla klas i const Example: FunctionComponent<ExampleProps> = (props) => {dla komponentów funkcji (gdzie ExamplePropsjest interfejs dla oczekiwanych właściwości). A następnie te typy mają wystarczającą ilość informacji, aby można było wywnioskować typ zwracany.
Nicholas Tower
Typy są zdefiniowane tutaj
Jonas Wilms
1
@NicholasTower Nasze reguły lintingu wymuszają jawne dostarczanie typu zwracanego i dlatego tak się pojawia (który IMHO jest dobrą rzeczą, ponieważ myślisz o wiele więcej o tym, co robisz, co pomaga zrozumieć, niż gdybyś po prostu pozwolił kompilatorowi wywnioskować wszystko) .
Golo Roden
W porządku, nie pomyślałem o zasadach szarpania.
Nicholas Tower
@JonasWilms Dzięki za link, ale nie sądzę, żeby to odpowiadało na moje pytania.
Golo Roden

Odpowiedzi:

182

Jaka jest różnica między JSX.Element, ReactNode i ReactElement?

ReactElement to obiekt z typem i właściwościami.

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

ReactNode to ReactElement, ReactFragment, ciąg znaków, liczba lub tablica ReactNodes, null, undefined lub wartość logiczna:

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 jest elementem ReactElement, z typem ogólnym dla właściwości i typem dowolnym. Istnieje, ponieważ różne biblioteki mogą implementować JSX na swój własny sposób, dlatego JSX jest globalną przestrzenią nazw, która jest następnie ustawiana przez bibliotekę, React ustawia ją w następujący sposób:

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

Przez przykład:

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

Dlaczego metody renderowania komponentów klas zwracają ReactNode, ale komponenty funkcji zwracają ReactElement?

Rzeczywiście, zwracają różne rzeczy. Components powrót:

 render(): ReactNode;

Funkcje są „składnikami bezstanowymi”:

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

W rzeczywistości wynika to z przyczyn historycznych .

Jak rozwiązać ten problem w odniesieniu do wartości null?

Wpisz to ReactElement | nulltak, jak robi to reakcja. Lub pozwól, aby Typescript wywnioskował typ.

źródło typów

Jonas Wilms
źródło
Dzięki za szczegółową odpowiedź! To doskonale odpowiada na pytanie 1, ale nadal pozostawia bez odpowiedzi na pytania 2 i 3. Czy możesz podać jakieś wskazówki na ich temat?
Golo Roden
@goloRoden jasne, jestem teraz trochę zmęczony i przewijanie typów na telefonie komórkowym zajmuje trochę czasu ...;)
Jonas Wilms
Cześć @JonasWilms. Jeśli chodzi o „Nie. ReactComponent jest zdefiniowany jako: render (): JSX.Element | null | false;”, gdzie to widzisz? Wygląda na to, że zwraca mi ReactNode ( github.com/DefinitelyTyped/DefinitelyTyped/blob/ ... ) Również drobna literówka, myślę, że ReactComponentpowinno być React.Componentlub Component.
Mark Doliner,
@MarkDoliner Dziwne, mógłbym przysiąc, że skopiowałem ten typ z pliku ... Cokolwiek, masz całkowitą rację, edytuję
Jonas Wilms
Chłopaki, czy jest gdzieś w sieci jakąś oficjalną dokumentację dotyczącą reakt && maszynopisu?
Goran_Ilic_Ilke
30

1.) Jaka jest różnica między JSX.Element, ReactNode i ReactElement?

ReactElement i JSX.Element są wynikiem wywołania React.createElementbezpośrednio lub poprzez transpilację JSX. Jest to obiekt z type, propsi key. JSX.Elementjest ReactElement, którego propsi typemają typ any, więc są mniej więcej takie same.

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

ReactNode jest używany jako typ zwracany render()w komponentach klas. Jest to również domyślny typ childrenatrybutu z PropsWithChildren.

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

W deklaracjach typu React wygląda to na bardziej skomplikowane , ale jest równoważne z:

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...)

Możesz przypisać prawie wszystko do ReactNode. Zwykle wolałbym silniejsze typy, ale mogą istnieć pewne ważne przypadki, aby go użyć.


2.) Dlaczego metody renderowania komponentów klas zwracają ReactNode, a komponenty funkcji zwracają ReactElement?

tl; dr: Jest to aktualna niekompatybilność typu TS niezwiązana z rdzeniem React .

  • Komponent klasy TS: wraca ReactNodez render(), bardziej pobłażliwy niż React / JS

  • Komponent funkcji TS: zwraca JSX.Element | null, bardziej restrykcyjny niż React / JS

W zasadzie render()w klasie React / JS komponenty obsługują te same typy zwracanych wartości, co komponent funkcji. W odniesieniu do TS, różne typy są niespójnymi typami, które są nadal utrzymywane z powodów historycznych i potrzeby kompatybilności wstecznej.

Idealnie byłoby, gdyby prawidłowy typ zwrotu wyglądał bardziej tak:

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

3.) Jak rozwiązać ten problem w odniesieniu do wartości null?

Niektóre opcje:
// 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>;

Uwaga: unikanie React.FC nie uchroni Cię przed JSX.Element | nullograniczeniem typu zwrotu.

Aplikacja Create React App została niedawno usuniętaReact.FC ze swojego szablonu, ponieważ ma pewne dziwactwa, takie jak niejawna {children?: ReactNode}definicja typu. Dlatego React.FCpreferowane może być oszczędne używanie .

W skrajnych przypadkach możesz dodać potwierdzenie typu lub fragmenty jako obejście:
const MyCompFragment: FunctionComponent = () => <>"Hello"</>
const MyCompCast: FunctionComponent = () => "Hello" as any 
// alternative to `as any`: `as unknown as JSX.Element | null`
ford04
źródło