Удаление обёртки провайдера: контекст отрисовки из компонентов сервера
Узнайте, как React 19.3 позволяет серверному компоненту напрямую отображать клиентский контекст, как Flight его сериализует, и какие ограничения по чтению и значениям всё ещё действуют.
Источники: React 19.3: Render a Context Directly in a Server Component, No Provider Wrapper
Почти в каждом проекте с React Server Components присутствует небольшой файл 'use client', единственная задача которого — создать Context и экспортировать компонент-провайдер, передающий один атрибут. Этот обёрточный компонент был необходим, поскольку серверным компонентам не разрешалось напрямую отображать сам Context. В React 19.3 было снято это ограничение, поэтому теперь сервер может напрямую отображать элемент Context, и обёрточный компонент можно удалить.
В этом руководстве рассматривается старый подход, новый подход, что на самом деле делает протокол Flight внутри системы, а также факторы, определяющие, сработает ли изменение в вашей текущей фреймворковой среде. Примеры требуют настоящей среды выполнения Server Components (фреймворка с поддержкой бандлера RSC и сервера), поэтому рассматривайте их как полный код для справки, а не как материал, который можно вставить в среду тестирования. Чтобы узнать больше о том, как сочетаются серверная и клиентская части приложения на основе RSC, ознакомьтесь с нашим обзором архитектуры React Server Components.
Старый подход: компонент, который лишь передаёт проп
Компоненты сервера не могут вызывать метод createContext, поскольку Context представляет собой механизм управления состоянием на стороне клиента. Однако дерево компонентов на стороне клиента регулярно нуждается в данных, уже имеющихся у сервера, таких как информация о залогиненном пользователе, флаги функций тенанта или локаль запроса. Единственным способом передачи данных между сторонами были атрибуты компонента клиента, который затем отрисовывал элемент provider.
Модуль клиента начинается с обычных директив и импортов.
'use client';
import { createContext } from 'react';
Затем создается Context, и экспортируется provider, чей код не выполняет ничего, кроме как передачи значения currentUser в качестве параметра value.
export const UserContext = createContext(null);// exists only to forward one prop
export function UserProvider({ currentUser, children }) {
return (
<UserContext value={currentUser}>
{children}
</UserContext>
);
}
Сторона компонентов сервера
Компонент сервера, отвечающий за макет, импортирует обертку вместо Context.
import { UserProvider } from './user-context';
На сервере загружается информация о пользователе, которая затем передается обертке в виде атрибута.
export async function Layout({ children }) {
const currentUser = await getCurrentUser();
return (
<UserProvider currentUser={currentUser}>
{children}
</UserProvider>
);
}
Если попытаться пропустить обёртку в React 19.2, возникает запутанная ошибка во время выполнения, начинающаяся со слов «Element type is invalid», а затем указывающая на то, что тип элемента с опозданием загрузки оказался Context вместо класса или функции. Кажется, что проблема в бандлере, но на самом деле причина кроется в рендерере. Context, импортированный через границу клиента, доходит до серверного рендерера в виде ссылки на клиентский код, которая загружается с опозданием, и версия 19.2 могла преобразовать такие ссылки только в компоненты. Объект Context никогда не считался допустимым результатом.
Новый подход: рендерить сам Context
В версии 19.3 клиентский модуль сокращается до директивы, импорта и одного экспорта.
'use client';
import { createContext } from 'react';
Компонента больше нет, остаётся только объект Context.
export const UserContext = createContext(null);
Рендеринг из лейаута
Теперь лейаут напрямую импортирует UserContext.
import { UserContext } from './user-context';
Структура JSX остаётся прежней, за исключением того, что элемент — это Context, а свойство называется value.
export async function Layout({ children }) {
const currentUser = await getCurrentUser();
return (
<UserContext value={currentUser}>
{children}
</UserContext>
);
}
Использование на клиенте
Для пользователей ничего не меняется. Любой клиентский компонент, расположенный ниже элемента layout, импортирует тот же Context и читает его с помощью use.
'use client';
import { use } from 'react';
import { UserContext } from './user-context';
Здесь меню с аватаром берёт пользователя из Context и отображает его изображение.
export function AvatarMenu() {
const user = use(UserContext);
return <img src={user.avatar} alt={user.name} />;
}
Обратите внимание на синтаксис React 19 в элементе layout: он отображает <UserContext value> вместо <UserContext.Provider value>. Формат с .Provider продолжает работать на клиенте, но на сервере отображается сам объект Context, поэтому в коде для сервера следует использовать более краткий формат.
Что делает Flight и чего не делает эта функция
Когда компонент сервера отрисовывает <UserContext value={currentUser}>, на сервере вообще не выполняется никакая логика контекста. Сериализатор Flight генерирует элемент, представляющий собой ссылку на клиентскую часть, указывающую на модуль user-context.js и его экспорт UserContext. Этот элемент содержит в качестве свойств { value: currentUser, children }. Эти свойства сериализуются точно так же, как и любые другие свойства, передаваемые на клиент. Как только браузер загружает этот модуль, ссылка на клиент превращается в реальный объект контекста, и React создает провайдер, хранящий переданное значение. Исправление, связанное с процессом отрисовки, задокументированное в facebook/react #35675, позволило коду с ленивым типированием принимать объект контекста наряду с функциями, memo и forwardRef.
Из такой конструкции следуют два последствия.
Значение должно быть сериализуемым
Значение Context подчиняется тем же правилам, что и любые свойства, передаваемые на клиентскую сторону. Допускаемыми типами являются примитивы, такие как строки и числа, обычные объекты и массивы, экземпляры Date, Map и Set, обещания (promises), элементы JSX и функции сервера. Обычные функции, экземпляры классов и всё, что имеет пользовательский прототип, не допускаются. Работает обычный запись currentUser; экземпляр UserService с методами — нет. В документации 'use client' приведен полный список.
Это не Context с серверной стороны
В петиции о внесении изменений прямо указано, что всё еще нельзя вызывать use(Context) внутри компонента сервера. У компонентов сервера нет способа прочитать Context, поскольку на сервере отсутствует провайдер для его чтения. Эта функция предназначена исключительно для упрощения передачи значения в дерево клиентской части.
Почему стоит убрать обертку
Раньше каждая общая переменная требовала одного файла и одного компонента. Это казалось незначительным, пока вы не начинали их считать: приложение с параметрами для пользователя, темы, региона, флагами функций, арендатора и аналитики включает шесть почти идентичных оболочек, каждая из которых представляет собой риск забыть какой-то параметр или ошибочно указать значение по умолчанию. Благодаря новой структуре провайдер находится прямо там, где загружаются данные, и такая категоризация «какие части работают на сервере, а какие — на клиенте» теряет один особый случай.
Реакция сообщества на это изменение была в основном облегчением, а не восторгом. Разработчики отмечали, что странно было с самого начала не поддерживать такой подход, ведь значение Context на самом деле — это просто данные, хранящиеся в памяти. Эта интуиция верна, и она объясняет, почему исправление в самом React было незначительным.
Важные моменты перед миграцией
- Ваша фреймворк-система должна включать пакеты версии 19.3 Flight. Изменения касаются файлов
react-server-dom-webpack,-turbopackи-parcel, а также компонента reconciler, а не только файлаreact. Проверьте версию React, заданную вашим фреймворком; несоответствие версий приводит к той же ошибке «Element type is invalid», описанной ранее. - Определяйте Context в файле, помеченном атрибутом
'use client'. Context, определённый в коде сервера, нельзя отрисовать таким образом, поскольку весь механизм зависит от наличия данного объекта в клиентском пакете. - Чтение данных на сервере по-прежнему невозможно. Компонент серверной части, который нуждается в этих данных, должен получать их в качестве аргумента или загружать самостоятельно; использование
use(Context)в таких случаях приводит к ошибке.
<UserContext value={getCurrentUser()}> без await приводит к передаче значения в поток, и потребители должны дважды распаковать его с помощью use(use(UserContext)). Это может быть полезной техникой потоковой обработки, но также легко допустить ошибку, упустив await.createContext(null), применяется там, где над потребителем нет поставщика, и размещение поставщика на сервере это не меняет.Пока оставаться с оберткой
Если ваша фреймворк-среда ещё не перешла на версию 19.3, оставьте обёртку. Паттерн UserProvider работает во всех версиях React 19 и в любой фреймворк-среде. При обновлении процесс миграции является простым:
- Удалите функцию-обёртку из клиентского модуля.
- Импортируйте Context в серверный компонент вместо обёртки.
- Переименуйте передаваемое свойство в
value.
Вы можете мигрировать файл за файлом по мере перехода вашей фреймворк-среды на версию 19.3. Нет необходимости преобразовывать всё сразу, и как только оба варианта будут работать, между ними не будет разницы во время выполнения.
Основные выводы
- В React 19.3 серверный компонент может отображать Context, экспортированный из модуля
'use client', поэтому обёртки-провайдеры с одной функцией становятся ненужными.