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
// ❌ Ошибка: может вернуть строку, но компонент ожи