Главная / Статьи / Оценка необходимости обновления до React 19: какие новые API заменят существующие решения

Оценка необходимости обновления до React 19: какие новые API заменят существующие решения

Практический обзор использования Server Actions, useOptimistic и React Compiler, включая важные ограничения и план по первому внедрению изменений React 19.

1579 слов

React 19 добавил больше функций API, чем любая предыдущая версия за многие годы, и из-за огромного количества информации трудно понять, что действительно важно для существующей базы кода. Для большинства команд несколько новых функций заменяют существующие решения, одна меняет подход к загрузке данных, а остальное можно отложить. Ниже приведены наиболее важные изменения с примерами кода до и после обновления, а также важные нюансы, которые легко упустить, чтобы вы могли спланировать обновление с учетом его реальной пользы.

use: чтение Promises и Context во время отрисовки

API use является самым значимым нововведением, и его поведение отличается от всех известных вам хуков. Обычные хуки должны вызываться на верхнем уровне компонента в одинаковом порядке при каждой отрисовке. use не подчиняется этому правилу: его можно вызывать после раннего возврата, внутри условия или цикла.

В приведённом ниже примере компонент возвращает вид для гостей, когда отсутствует userId, и только затем загружает данные пользователя:

// React 19 — use() can be called inside conditionals and loops
function UserProfile({ userId }) {
  if (!userId) return <GuestView />;

  // This is valid in React 19
  const user = use(fetchUser(userId));

  return <div>{user.name}</div>;
}

Настоящая мощность заключается в том, что принимает функция use: Promise или Context. При получении Promise React приостанавливает работу компонента до его решения. Приведённое ниже сравнение показывает, насколько много кода исчезает. В старой версии данные и флаг загрузки хранятся в состоянии, запрос выполняется через effect, а спиннер рендерится вручную; в версии React 19 значение читается напрямую:

// Before React 19
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUser(userId).then(data => {
      setUser(data);
      setLoading(false);
    });
  }, [userId]);

  if (loading) return <Spinner />;
  return <div>{user.name}</div>;
}

// React 19
function UserProfile({ userId }) {
  const user = use(fetchUser(userId));
  return <div>{user.name}</div>;
}

Работа не исчезает, а просто перемещается: ближайший контейнер <Suspense> отображает запасной вариант, пока Promise находится в состоянии ожидания, а ближайший контейнер обработки ошибок справляется с их возникновением. Компонент описывает только путь успешного выполнения.

Promise должен быть стабильным

use ожидает один и тот же объект Promise при каждом перерисовании. Если функция fetchUser(userId) создаёт новый объект Promise при каждом перерисовании, React снова приостанавливает его выполнение, что приводит к многократным запросам или к ситуации, когда компонент так и не достигает состояния готовности; в клиентских компонентах React выдаёт предупреждения о создании незакэшированных объектов Promise во время перерисования. Поэтому рассматривайте приведённые примеры лишь как иллюстрации формата. Объект Promise должен поступать от источника, который его кэширует: библиотеки данных с поддержкой Suspense, загрузчиков фреймворков или серверных компонентов, которые инициируют запрос и передают объект Promise в качестве свойства. Иногда рекомендуется оборачивать вызов в useMemo, но React не гарантирует сохранение мемоизированных значений, поэтому это не является надёжным способом кэширования.

Действия на сервере как функция React

Пользователи App Router в Next.js уже знакомы с Server Actions. В React 19 они стали частью самого React (в документации их теперь называют Server Functions) и доступны в любой фреймворке, поддерживающем Server Components.

В примере определена асинхронная функция, помеченная атрибутом 'use server', которая вставляет пользователя и обновляет список, после чего эта функция передается в свойство action формы:

// Server Action — runs on the server, called from the client
async function submitForm(formData) {
  'use server';

  const name = formData.get('name');
  await db.users.create({ name });
  revalidatePath('/users');
}

// Client component
function UserForm() {
  return (
    <form action={submitForm}>
      <input name="name" />
      <button type="submit">Add User</button>
    </form>
  );
}

Свойство action тега <form> теперь принимает функцию, включая асинхронную; React запускает её при отправке формы и передаёт ей объект FormData. Состояние ожидания, оптимистичные обновления и ошибки реализуются с помощью соответствующих API: useFormStatus или useActionState для отслеживания состояния ожидания и результата, useOptimistic для мгновенной обратной связи, а также барьеры ошибок для обработки сбоев. Вы пишете логику изменений; React координирует остальные аспекты.

В реальном приложении необходимо скорректировать одну деталь из этого фрагмента. Функция с встроенной директивой 'use server' может быть определена только в серверном компоненте. Если UserForm является клиентским компонентом, переместите функцию submitForm в отдельный файл, в верхней части которого будет указана директива 'use server', а затем импортируйте этот файл.

Там, где серверные компоненты удаляют статический интерфейс из пакета клиента, серверные действия устраняют вручную написанные маршруты API для выполнения изменений: меньше кода, меньше запросов. Тем не менее каждое действие следует рассматривать как публичный конечный пункт, а внутри него необходимо проверять входные данные и авторизацию.

useOptimistic: мгновенная обратная связь без дублирования состояния

Ранее отрисовка результата до подтверждения с сервера требовала вручную управления дополнительным состоянием. Функция useOptimistic берет текущее состояние и функцию обновления, похожую на редьюсер, возвращает состояние для отрисовки вместе с функцией, применяющей оптимистичные изменения. При добавлении задачи в список создается временная запись с меткой «в ожидании», которая отображается в слегка размытом виде до завершения сохранения:

function TodoList({ todos }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (currentTodos, newTodo) => [...currentTodos, newTodo]
  );

  async function handleAdd(text) {
    addOptimisticTodo({ id: 'temp', text, pending: true });
    await saveTodo(text); // Server Action
  }

  return (
    <ul>
      {optimisticTodos.map(todo => (
        <li key={todo.id} style={{ opacity: todo.pending ? 0.7 : 1 }}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

При вызове addOptimisticTodo React сразу отображает список в оптимистичном режиме. Когда соответствующая операция завершается, оптимистичные данные удаляются, и React снова отрисовывает список на основе свойства todos, которое к этому моменту должно содержать сохраненную запись.

Раньше вы хранили подтверждённые и ожидающие обработки копии данных и вручную устраняли несоответствия, когда они возникали; теперь этот жизненный цикл контролируется механизмом hook. Важны два условия. Во-первых, оптимистичное обновление должно происходить внутри операции или перехода, например, через функцию, переданную в свойство action формы, или обёрнутую в startTransition; вызов её из обычного обработчика событий приведёт к предупреждению от React, и обновление может не отобразиться. Во-вторых, родительский элемент должен действительно получать обновлённый набор todos после сохранения, обычно в результате повторной верификации; иначе новый элемент исчезнет при сбросе оптимистичного состояния. Временные идентификаторы вроде 'temp' также могут привести к конфликтам, если два элемента добавляются быстро, поэтому необходимо генерировать уникальные идентификаторы. Эти крайние случаи рассматриваются в пяти способах сбоя при использовании оптимистичного отката.

Компилятор React: мемоизация по умолчанию

Компилятор React, ранее называвшийся React Forget, появился вместе с React 19 как опциональный этап сборки и оказал наибольшее влияние на повседневный код. Он встраивает мемоизацию на этапе сборки, благодаря чему значения и функции-обратные вызовы переиспользуются, когда входные данные не изменились, а дочерние элементы пропускают повторную отрисовку при одинаковых свойствах. Ручное использование useMemo, useCallback и React.memo становится в основном ненужным, как показывает следующее сравнение:

// Before: manual memoization required
const expensiveValue = useMemo(() => compute(a, b), [a, b]);
const stableCallback = useCallback(() => doSomething(id), [id]);
const MemoizedChild = React.memo(ChildComponent);

// After React Compiler: write normal code
const expensiveValue = compute(a, b);
const handleClick = () => doSomething(id);
// ChildComponent renders only when its props change - automatically

Вам по-прежнему необходимо понимать, когда и почему компоненты перерисовываются, но ручные способы обхода этой проблемы становятся гораздо менее важными. Наш обзор шаблонов, вызывающих ненужную перерисовку по-прежнему остается полезным справочным материалом.

Для новых проектов компилятор является надежным стандартным решением. В существующем коде его следует внедрять осторожно: он предполагает, что компоненты являются «чистыми» и соответствуют правилам React, поэтому код, изменяющийся во время отрисовки, может вести себя иначе после компиляции. Включайте его постепенно, запускайте тесты и сначала используйте плагин ESLint для выявления нарушений. Проверьте актуальную документацию React, чтобы узнать статус выпуска и поддерживаемые версии React.

Что внедрять и в каком порядке

Немедленная польза, низкий риск

  • useOptimistic там, где формы взаимодействуют со состоянием сервера.
  • Server Actions, если вы используете Next.js 14 или новее и хотите заменить API-маршруты для выполнения операций изменения данных.

Что стоит запланировать

  • use, когда ваш слой данных генерирует стабильные, закэшированные Promises; он хорошо сочетается с Suspense.
  • Использование компилятора React в новых проектах или сначала в хорошо протестированных частях существующего приложения.

Для большинства приложений это не срочно

  • Изменения в объекте ref: теперь ref можно передавать как обычное свойство функциональным компонентам, поэтому forwardRef больше не требуется.
  • Улучшения в работе с контекстом, которые в основном касаются улучшения опыта разработчика, а не добавления нового поведения.
  • Поддержка метаданных документа, что позволяет компонентам отображать теги <title> и <meta>, которые React вставляет в начало документа.

Основные выводы

  • React 19 — это развитие существующей версии, а не радикальная переработка. Его новые API формализуют подходы, которые команды по разработке уже используют вручную: оптимистичные обновления, изменения на сервере и условное чтение данных.
  • use переносит обработку загрузки и ошибок в Suspense и границы ошибок, но эффективно работает только с Promises, которые кэшируются вне процесса отрисовки.
  • Server Actions и useOptimistic хорошо сочетаются друг с другом: первый обрабатывает запись данных, а второй скрывает свою задержку.
  • Компилятор делает мемоизацию стандартной практикой, при условии соблюдения компонентами правил React.
  • Важный вопрос заключается не в том, стоит ли обновляться, а в том, какой из этих инструментов заменит временное решение, которое вы сейчас используете. Начните с этого.

    Связанные материалы

  • Асинхронные формы в React 19 с use(), useActionState, useFormStatus и useOptimistic — Как функции use(), useActionState, useFormStatus и useOptimistic заменяют ручную обработку загрузки данных и флагов ошибок в React 19, а также какие подводные камни скрываются за каждым из этих хуков.