Главная / Статьи

Удаление обёртки провайдера: контекст отрисовки из компонентов сервера

Узнайте, как React 19.3 позволяет серверному компоненту напрямую отображать клиентский контекст, как Flight его сериализует, и какие ограничения по чтению и значениям всё ещё действуют.

Издатель: REACTAPP.TOP7 мин чтения1602 слов

Источники: 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', поэтому обёртки-провайдеры с одной функцией становятся ненужными.
  • По сути, Flight отправляет ссылку на клиента вместе с сериализованными параметрами; никакое поведение Context не выполняется на сервере.
  • Значение должно соответствовать обычным правилам сериализации, и Server Components по-прежнему не могут читать Context.
  • Убедитесь, что ваша фреймворк-среда объединяет соответствующие пакеты Flight перед удалением оберток, и следите за случайными значениями типа promise.
  • Для получения дополнительной информации прочитайте записку об обновлении React 19.3 на сайте react.dev и справочник Server Components.