Коли використовувати JSX.Element проти ReactNode проти ReactElement?


168

Зараз я переношу додаток React у TypeScript. Поки це працює досить добре, але у мене проблема з типами повернення моїх renderфункцій, відповідно моїх функціональних компонентів.

До цього часу я завжди використовував JSX.Elementяк тип return, тепер це більше не працює, якщо компонент вирішить нічого не відображати, тобто return null, оскільки nullне має дійсного значення для JSX.Element. Це був початок моєї подорожі, тому що зараз я шукав в Інтернеті і виявив, що ви повинні використовувати ReactNodeзамість цього, що також включає, nullа також кілька інших речей, які можуть статися. Здавалося, це кращий вибір.

Однак зараз при створенні функціонального компонента TypeScript скаржиться на ReactNodeтип. Знову ж таки, після деяких пошуків я виявив, що для функціональних компонентів слід використовувати ReactElementзамість них. Однак, якщо я це зроблю, проблема сумісності зникне, але тепер TypeScript знову скаржиться на те, що nullне є дійсним значенням.

Отже, якщо коротко, у мене є три запитання:

  1. У чому різниця між JSX.Element, ReactNodeі ReactElement?
  2. Чому renderповертаються методи компонентів класу ReactNode, а повертаються функціональні компоненти ReactElement?
  3. Як я можу це вирішити стосовно null?

1
Я здивований, що це може виникнути, оскільки зазвичай вам не потрібно вказувати тип повернення з компонентами. Який тип підпису ви робите для компонентів? Має виглядати приблизно так, як class Example extends Component<ExampleProps> {для класів, так і const Example: FunctionComponent<ExampleProps> = (props) => {для функціональних компонентів (де ExamplePropsінтерфейс для очікуваного реквізиту). І тоді ці типи мають достатньо інформації, що можна визначити тип повернення.
Миколаївська вежа

Типи визначено тут
Jonas Wilms

1
@NicholasTower Наші правила зв'язування чітко забезпечують надання типу повернення, і тому це виникає (що IMHO - це добре, тому що ти більше думаєш про те, що ти робиш, що допомагає зрозуміти, ніж якщо ти просто дозволяєш компілятору робити все висновки) .
Голо Роден

Досить справедливо, я не думав обмежувати правила.
Миколаївська вежа

@JonasWilms Дякую за посилання, але я не думаю, що це відповідає на мої запитання.
Голо Роден

Відповіді:


182

У чому різниця між JSX.Element, ReactNode і ReactElement?

ReactElement - це об’єкт із типом та реквізитом.

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

ReactNode - це ReactElement, ReactFragment, рядок, число або масив ReactNodes, або null, або undefined, або логічне значення:

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 - це ReactElement, із загальним типом для реквізитів і типом будь-яким. Він існує, оскільки різні бібліотеки можуть реалізувати JSX по-своєму, тому JSX - це глобальний простір імен, який потім задається бібліотекою, React встановлює його так:

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

На прикладі:

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

Чому методи візуалізації компонентів класу повертають ReactNode, а функціональні компоненти повертають ReactElement?

Дійсно, вони повертають різні речі. Componentповернення s:

 render(): ReactNode;

А функції є "компонентами без стану":

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

Це насправді пов’язано з історичними причинами .

Як я можу це вирішити щодо нуля?

Введіть його так ReactElement | nullсамо, як це робить реакція. Або нехай Typescript виводить тип.

джерело для типів


Дякую за детальну відповідь! Це чудово відповідає на питання 1, але все ще залишає питання 2 і 3 без відповіді. Чи можете ви дати їм деякі вказівки щодо них, будь ласка?
Голо Роден

@goloRoden впевнений, я просто трохи втомився зараз, і потрібно деякий час, щоб прокрутити типи на мобільному ...;)
Jonas Wilms

Привіт @JonasWilms. Щодо "Вони цього не роблять. ReactComponent визначається як: render (): JSX.Element | null | false;", де ви це бачите? Схоже, він повертає мені ReactNode ( github.com/DefinitelyTyped/DefinitelyTyped/blob/… ) Також незначна помилка, я думаю, ReactComponentповинна бути React.Componentабо Component.
Mark Doliner

@MarkDoliner Weird, я міг би поклястись, що скопіював цей тип із файлу ... Що б там не було, ти маєш повну рацію, я відредагую
Jonas Wilms

Хлопці, чи є де-небудь в Інтернеті якась офіційна документація на машинопис React &&?
Goran_Ilic_Ilke

30

1.) У чому різниця між JSX.Element, ReactNode і ReactElement?

ReactElement та JSX.Element є результатом виклику React.createElementбезпосередньо або через трансляцію JSX. Це об'єкт з type, propsі key. JSX.Elementє ReactElement, чиї propsі typeмають тип any, тому вони більш-менш однакові.

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

ReactNode використовується як тип повернення для render()компонентів класу. Це також тип за замовчуванням для childrenатрибута з PropsWithChildren.

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

Це виглядає складніше в оголошеннях типу React , але еквівалентно :

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

Ви можете призначити майже все ReactNode. Зазвичай я віддаю перевагу більш сильним типам, але можуть бути деякі вагомі випадки, щоб використовувати його.


2.) Чому методи візуалізації компонентів класу повертають ReactNode, а функціональні компоненти повертають ReactElement?

tl; dr: Це поточна несумісність типу TS, не пов'язана з основним React .

  • Компонент класу TS: повертається ReactNodeз render(), більш дозвільним, ніж React / JS

  • Компонент функції TS: повертає JSX.Element | null, більш обмежувальний, ніж React / JS

В принципі, render()у React / JS компоненти класу підтримують ті самі типи повернення , що і функціональний компонент. Що стосується TS, то різні типи - це невідповідність типу, яка все ще зберігається через історичні причини та необхідність зворотної сумісності.

В ідеалі дійсний тип повернення , мабуть, виглядатиме приблизно так:

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

3.) Як я можу це вирішити щодо нуля?

Деякі варіанти:
// 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>;

Примітка. Уникнення React.FC не врятує вас від JSX.Element | nullобмеження типу повернення.

Програма Create React нещодавно випалаReact.FC зі свого шаблону, оскільки вона має деякі химерності, як неявне {children?: ReactNode}визначення типу. Отже, React.FCекономне використання може бути кращим.

У крайових випадках можна додати твердження типу або Фрагменти як обхідне рішення:
const MyCompFragment: FunctionComponent = () => <>"Hello"</>
const MyCompCast: FunctionComponent = () => "Hello" as any 
// alternative to `as any`: `as unknown as JSX.Element | null`
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.