Коротко: Разбираем причины ошибки Promise<Element> is not a valid JSX element в React и Next.js. Пошаговые способы исправления типов и настройки TypeScript.
При переходе на React Server Components (RSC) или разработке страниц в Next.js App Router разработчики регулярно сталкиваются с ошибкой тайпчекера: 'Promise<Element>' is not a valid JSX element (или ее расширенным вариантом: 'Component' cannot be used as a JSX component. Its return type 'Promise<Element>' is not a valid JSX element).
// Пример компонента, вызывающего ошибку компиляции
async function UserProfile({ userId }: { userId: string }) {
const user = await fetchUserData(userId);
return <div>{user.name}</div>;
}
// Ошибка при вызове в JSX:
// 'UserProfile' cannot be used as a JSX component.
// Its return type 'Promise<JSX.Element>' is not a valid JSX element.
export default function Page() {
return <UserProfile userId="123" />;
}
Эта проблема связана не с логикой рендеринга самого React, а с тем, как TypeScript проверяет возвращаемые типы JSX-элементов. Ниже разобран механизм возникновения ошибки и рабочие способы ее устранения — от обновления зависимостей до архитектурного разделения компонентов.
В чем суть проблемы и почему возникает ошибка
Конфликт синхронной модели JSX и промисов (async/await)
Исторически любой React-компонент представлял собой синхронную функцию. Тайпчекер TypeScript ожидал, что функциональный компонент вернет значение типа JSX.Element либо ReactNode (примитивы, массивы или null).
Когда функция объявляется с ключевым словом async, JavaScript автоматически оборачивает возвращаемое значение в объект Promise. Сигнатура функции меняется с () => JSX.Element на () => Promise<JSX.Element>. Ранние версии TypeScript и определений @types/react не допускали промисы в качестве валидного узла JSX-дерева, поэтому статический анализ завершался ошибкой.
Роль TypeScript 5.1 и механизма JSX.ElementType
До релиза TypeScript 5.1 компилятор жестко требовал, чтобы тип возвращаемого значения функционального компонента строго наследовался от JSX.Element. Даже когда серверный рантайм React уже поддерживал асинхронные компоненты, TypeScript запрещал их использование в разметке.
Начиная с TypeScript 5.1 был внедрен механизм JSX.ElementType. Он позволил библиотекам гибко задавать типы, допустимые в роли JSX-тегов. В сочетании с обновленными пакетами @types/react компилятор научился распознавать асинхронные функции, возвращающие Promise<ReactNode>, как валидные серверные компоненты.
Главные причины появления ошибки на практике
1. Устаревшие версии TypeScript или пакетов @types/react
Наиболее частая причина: проект использует актуальный фреймворк (Next.js 13/14/15 или React 18/19), но в файле package.json зафиксированы старые версии typescript (ниже 5.1) или @types/react (ниже 18.2.43). В этом случае среда выполнения готова к асинхронным компонентам, а компилятор блокирует сборку.
2. Попытка сделать асинхронным клиентский компонент ('use client')
Асинхронными могут быть только React Server Components (RSC). Если файл помечен директивой 'use client', React ожидает синхронного рендеринга на стороне браузера. Объявление клиентского компонента как async function приводит к ошибкам типов и сбоям в рантайме.
'use client';
// ОШИБКА: Клиентский компонент не может быть асинхронной функцией
export default async function ClientWidget() {
const data = await fetch('/api/data');
return <div>{data.title}</div>;
}
3. Дублирование типов в node_modules и монорепозиториях
В монорепозиториях (Turborepo, Nx, pnpm workspaces) или при наличии вложенных транзитивных зависимостей часто возникает конфликт версий @types/react. Редактор кода или сборщик может подхватить устаревший файл деклараций из соседнего пакета, игнорируя глобально установленный пакет.
Пошаговые способы решения
Решение 1. Обновление TypeScript и типов React (рекомендуемый путь)
Для штатной поддержки асинхронных компонентов обновите компилятор и сопутствующие типы:
- Убедитесь, что версия
typescriptв проекте — не ниже 5.1.3, а пакеты@types/reactи@types/react-dom— не ниже 18.2.43 (или соответствуют React 19). - Запустите команду обновления пакетов:
# Для npm
npm install -D typescript@latest @types/react@latest @types/react-dom@latest
# Для pnpm
pnpm add -D typescript@latest @types/react@latest @types/react-dom@latest
# Для yarn
yarn add -D typescript@latest @types/react@latest @types/react-dom@latest
- Перезапустите TypeScript Server в редакторе (VS Code):
- Нажмите комбинацию клавиш
Ctrl + Shift + P(илиCmd + Shift + Pна macOS). - Выполните команду
TypeScript: Restart TS Server. - Убедитесь, что IDE использует версию из проекта: вызовите
TypeScript: Select TypeScript Versionи переключитесь на Use Workspace Version.
- Нажмите комбинацию клавиш
Решение 2. Корректное разделение Server и Client Components
Если компонент использует состояние, эффекты или браузерные события (useState, useEffect, onClick), он должен оставаться клиентским и синхронным. Асинхронную загрузку данных в таком случае переносят на уровень серверного родителя.
Архитектурный паттерн (Server Container -> Client Presentation):
- Серверный компонент выполняет асинхронный запрос:
// ServerPage.tsx (Server Component по умолчанию)
import ClientForm from './ClientForm';
export default async function ServerPage() {
const initialData = await fetchFormData();
return <ClientForm initialData={initialData} />;
}
- Клиентский компонент принимает готовые данные через props:
// ClientForm.tsx
'use client';
import { useState } from 'react';
interface ClientFormProps {
initialData: { title: string };
}
export default function ClientForm({ initialData }: ClientFormProps) {
const [data, setData] = useState(initialData);
return (
<form>
<input
value={data.title}
onChange={(e) => setData({ title: e.target.value })}
/>
</form>
);
}
Если данные требуется загружать напрямую в клиентском компоненте, используйте хук use() в связке с Suspense или библиотеки управления серверным состоянием (TanStack Query, SWR):
'use client';
import { use } from 'react';
export default function UserInfo({ userPromise }: { userPromise: Promise<{ name: string }> }) {
// Хук use() синхронно разворачивает переданный промис внутри Suspense
const user = use(userPromise);
return <div>{user.name}</div>;
}
Решение 3. Временное подавление через @ts-expect-error
Если проект заблокирован внешними зависимостями и моментальный апгрейд TypeScript невозможен, используйте точечное подавление директивой @ts-expect-error:
// AsyncWidget.tsx
async function AsyncWidget() {
const data = await getData();
return <div>{data.text}</div>;
}
// Page.tsx
export default function Page() {
return (
<main>
{/* @ts-expect-error Server Component Async Return Type */}
<AsyncWidget />
</main>
);
}
Обратите внимание: данный метод допустим только как временная мера перед плановым рефакторингом зависимостей.
Особенности работы в Next.js App Router
В App Router (app/) все компоненты по умолчанию исполняются на сервере. Асинхронные страницы (page.tsx) и лэйауты (layout.tsx) являются стандартным шаблоном.
Рекомендации по конфигурации проекта:
- Конфигурация
tsconfig.json: убедитесь, что параметрjsxустановлен в"preserve", а также включен флаг пропуска проверки внешних библиотек"skipLibCheck": true.
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
- Совместимость с UI-библиотеками: если устаревший UI-провайдер принимает в качестве
childrenтолько строгийJSX.Element, оберните асинхронный вызов в компонент-обертку или используйтеSuspense.
Чек-лист для устранения ошибки в кодовой базе
- [ ] В
package.jsonустановлена версияtypescript >= 5.1.3. - [ ] Пакеты
@types/reactи@types/react-domобновлены до актуальных версий. - [ ] В настройках IDE (VS Code) выбрана версия TypeScript из рабочей области (
Workspace Version). - [ ] Компоненты, содержащие директиву
'use client', не имеют модификатораasync. - [ ] В
tsconfig.jsonактивирован флаг"skipLibCheck": trueи задан современный"moduleResolution"(bundlerилиnode16). - [ ] При наличии монорепозитория выполнена дедупликация пакетов (
pnpm dedupe/npm dedupe).
Часто задаваемые вопросы (FAQ)
Можно ли сделать асинхронным клиентский компонент?
Нет. Клиентские компоненты должны возвращать разметку синхронно. Для асинхронных операций в них применяются хуки (useEffect, use), TanStack Query или получение данных через серверные компоненты-контейнеры.
Помогает ли приведение типов вида as unknown as JSX.Element?
Кастинг типов устраняет сообщение об ошибке, но снижает безопасность кода и маскирует реальное состояние типов. Основным решением остается синхронизация версий typescript и @types/react.
Что делать, если зависимости обновлены, но редактор продолжает подсвечивать ошибку?
Это указывает на кэш TS Server в редакторе. Нажмите Ctrl + Shift + P, выберите TypeScript: Restart TS Server и перепроверьте, что в статусной строке IDE используется версия из node_modules проекта, а не глобальная версия редактора.
Почему в Pages Router асинхронные компоненты вызывают ошибку?
В каталоге pages/ (Pages Router) действует классическая модель рендеринга React, не поддерживающая React Server Components на уровне страниц. Для загрузки данных там предусмотрены функции getServerSideProps и getStaticProps.
Заключение
Ошибка 'Promise<Element>' is not a valid JSX element отражает эволюцию экосистемы React и ее переход к серверным компонентам. Проблема решается обновлением связки typescript (5.1+) и @types/react, настройкой правильной версии компилятора в IDE и разделением серверной логики получения данных и клиентского интерактивного UI.




.svg.webp)


