JSX.Element, ReactNode, ReactElement: когда что использовать в TypeScript?

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

JSX.Element vs ReactNode vs ReactElement: Битва типов, которую вы обязаны выиграть

Привет, коллега! Если ты пишешь на React с TypeScript, то наверняка сталкивался с ситуацией, когда компилятор начинает ругаться на типы возвращаемых значений. "Expected JSX.Element, got ReactNode" — знакомая ошибка? А может, ты видел ReactElement в дженериках и задумывался: "А в чем вообще разница?"

Спойлер: это не просто академические термины. Правильный выбор типа может ускорить твой код, предотвратить баги и сделать компоненты более предсказуемыми. Сегодня мы разберем эту тему на реальных примерах, чтобы ты больше никогда не путал эти три типа. Поехали!

Краткий ликбез: что есть что

Давай начнем с основ. Все три типа — это части одной экосистемы, но с разной степенью абстракции.

ReactElement

Это базовый строительный блок. ReactElement — это объект с типом, пропсами и ключом. Он представляет собой "чертеж" элемента, который React потом превратит в настоящий DOM-узел. Если ты пишешь React.createElement('div', null, 'Hello'), то получаешь именно ReactElement.

import React from 'react';

const element: React.ReactElement = React.createElement('div', { className: 'box' }, 'Привет!');
// Тип: ReactElement<{ className: string }, 'div'>

Важно: ReactElement является дженериком. Ты можешь указать тип пропсов и тип элемента:

type MyElement = React.ReactElement<{ name: string }, 'button'>;

JSX.Element

Это расширение ReactElement, которое добавляет специфику JSX. По сути, JSX.Element — это ReactElement<any, any>. Когда ты пишешь <div /> в JSX, TypeScript видит именно JSX.Element.

const jsxElement: JSX.Element = <div>JSX.Element</div>;

Главное отличие от ReactElement — JSX.Element не параметризуется. Это просто "любой JSX-элемент".

ReactNode

А вот это самый широкий тип. ReactNode включает в себя:

  • ReactElement
  • Строки
  • Числа
  • Booleans (true/false)
  • null и undefined
  • Массивы из перечисленного выше
  • React Portal

Простыми словами: ReactNode — это всё, что React может отрендерить. Именно этот тип используется для children в компонентах.

type ReactNode = 
  | React.ReactElement
  | string
  | number
  | boolean
  | null
  | undefined
  | React.ReactNode[];

Когда что использовать? Практические сценарии

Теперь самое интересное — давай разберем, какой тип выбрать в реальных ситуациях.

Сценарий 1: Возвращаемое значение компонента

Твой компонент возвращает JSX. Какой тип указать?

// ❌ Плохо: слишком общий тип
function MyComponent(): ReactNode {
  return <div>Hello</div>;
}

// ✅ Хорошо: используем JSX.Element
function MyComponent(): JSX.Element {
  return <div>Hello</div>;
}

// ✅ Отлично: если нужна строгая типизация пропсов
function MyComponent(): React.ReactElement<{ title: string }> {
  return <div>Hello</div>;
}

Почему? Компонент всегда возвращает JSX (или null, если ничего не рендерит). ReactNode слишком широк — он позволяет вернуть строку, число или массив, что может привести к неожиданным ошибкам.

Сценарий 2: children в компоненте

Здесь ReactNode — твой лучший друг. Дочерние элементы могут быть чем угодно: строка, другой компонент, null (условный рендеринг) или массив.

interface CardProps {
  title: string;
  children: React.ReactNode; // ✅ Правильно
}

function Card({ title, children }: CardProps): JSX.Element {
  return (
    <div className="card">
      <h2>{title}</h2>
      {children}
    </div>
  );
}

// Использование:
<Card title="Привет">
  <p>Текст</p>
  {null} // ✅ Работает
  {42}   // ✅ Работает (React отрендерит число)
</Card>

Сценарий 3: Фабрики и HOC

Если ты пишешь функцию, которая создает элементы (например, HOC или render prop), используй ReactElement — он дает больше контроля над типом пропсов.

// Функция-фабрика для создания кнопок
function createButton(label: string): React.ReactElement<{ onClick: () => void }> {
  return <button onClick={() => alert(label)}>{label}</button>;
}

// Теперь TypeScript знает, что у кнопки есть onClick
const myButton = createButton('Нажми меня');

Производительность: выбираем правильный тип

Ты удивишься, но выбор типа может влиять на производительность. Нет, не напрямую (типы стираются во время компиляции), а косвенно — через оптимизацию рендеринга.

Проблема с ReactNode

Когда ты используешь ReactNode для пропсов, компилятор не может предсказать, что именно будет передано. Это может привести к лишним ререндерам, если React не сможет оптимизировать дерево.

// ❌ Потенциально медленнее
interface LazyComponentProps {
  content: React.ReactNode; // React не знает, что внутри
}

// ✅ Быстрее: явно указываем тип
interface FastComponentProps {
  content: React.ReactElement<{ data: string }>; // React знает структуру
}

Совет: Если ты знаешь, что children всегда будет одним компонентом с определенными пропсами — используй ReactElement с дженериком. Это позволит React лучше оптимизировать виртуальное DOM-дерево.

Ленивая загрузка и React.lazy

При работе с React.lazy всегда используй JSX.Element для возвращаемого значения:

const LazyComponent = React.lazy(() => import('./HeavyComponent'));

function App(): JSX.Element {
  return (
    <Suspense fallback={<div>Загрузка...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

Типичные ошибки и как их избежать

Ошибка 1: ReactNode в return

// ❌ Ошибка: может вернуть строку, но компонент ожи                

Блоги

Рекомендаций по книгам