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

React 19.3: что за пределами заголовков — отправитель данных, атрибуты полноэкранного режима и размер пакета

Практический обзор менее значительных изменений в React 19.3: свойство submitter, параметры события fullscreen, параллельные переходы, метод onReset, важные исправления багов и реальное увеличение размера.

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

Источники: Everything Else in React 19.3: submitter, Fullscreen Events, Independent Transitions, the Bug Fixes and the Bundle Cost

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

Определение кнопки, отправляющей форму

Форма с двумя кнопками отправки, например Save и Save & close, должна знать, на которую из них нажал пользователь. Браузеры уже давно отвечают на этот вопрос: встроенное событие SubmitEvent содержит свойство submitter, указывающее на кнопку, которая инициировала отправку. Однако синтетическое событие в React не передавало это свойство. Обычные решения заключались в чтении значения e.nativeEvent.submitter или, что менее изящно, в сохранении информации о последней нажатой кнопке в состоянии компонента с надеждой, что отправка через клавиатуру не пройдет незамеченной. В React 19.3 к синтетическому событию отправки было добавлено свойство submitter (#35590).

Самый простой способ увидеть разницу — это протестировать одну и ту же форму в обеих версиях. Демонстрация начинается с импортов; импорт из пространства имен нужен лишь для того, чтобы компонент мог вывести значение React.version.

import { useState } from 'react';
import * as React from 'react';

Этот компонент блокирует стандартную навигацию, затем записывает версию React и значение e.submitter в состояние. Каждая кнопка имеет своё собственное значение value, что именно используется для разветвления в реальном коде.

export default function App() {
  const [msg, setMsg] = useState('press either button');
  return (
    <form onSubmit={e => {
      e.preventDefault();
      setMsg(`${React.version}: e.submitter → ${e.submitter ? e.submitter.value : String(e.submitter)}`);
    }}>
      <button value="save">Save</button>{' '}
      <button value="save-close">Save &amp; close</button>
      <p className="msg">{msg}</p>
    </form>
  );
}

Одно правило форматирует строку вывода шрифтом моноширинного типа.

.msg { margin: 10px 0 0; font-family: ui-monospace, monospace; }

Тот же формуляр в версии 19.3

Версия демо для 19.3 полностью идентична, и это и есть суть: меняется только версия React. Импорты остаются прежними.

import { useState } from 'react';
import * as React from 'react';

То же самое касается компонента.

export default function App() {
  const [msg, setMsg] = useState('press either button');
  return (
    <form onSubmit={e => {
      e.preventDefault();
      setMsg(`${React.version}: e.submitter → ${e.submitter ? e.submitter.value : String(e.submitter)}`);
    }}>
      <button value="save">Save</button>{' '}
      <button value="save-close">Save &amp; close</button>
      <p className="msg">{msg}</p>
    </form>
  );
}

То же самое касается таблицы стилей.

.msg { margin: 10px 0 0; font-family: ui-monospace, monospace; }

В версии 19.2 в сообщении отображается undefined, поскольку у синтетического события нет такого поля. В версии 19.3 отображается save или save-close, в зависимости от кнопки. Учитывайте, какой путь кода этим затронут. Если данные отправляются через действия формы вместо обработчика onSubmit, React уже считывал элемент отправки при формировании объекта FormData (#29028), поэтому пара name и value нажатой кнопки уже находилась в данных действия. Новое свойство имеет значение для обработчиков событий.

Практический совет: значение элемента отправки будет полезно только в том случае, если вы действительно установили value (а обычно и name) для каждой кнопки. Кнопка без этих значений возвращает пустую строку, что легко можно принять за отсутствие функции.

События полноэкранного режима как свойства первого класса

API полноэкранного режима генерирует события fullscreenchange и fullscreenerror для соответствующего элемента. React не распознавал эти имена событий, поэтому компонентам вроде видеоплееров приходилось вручную добавлять обработчики с помощью addEventListener внутри функций эффектов и помнить удалять их при очистке. В React 19.3 были добавлены свойства onFullscreenChange и onFullscreenError в качестве обычных параметров (#34621).

Для демонстрации требуется ссылка на элемент, переходящий в полноэкранный режим, а также состояние для хранения журнала событий.

import { useRef, useState } from 'react';

Обратите внимание, что оба обработчика находятся непосредственно внутри div. Обработчик изменений проверяет значение document.fullscreenElement, чтобы определить, вошел ли элемент в режим полноэкранного отображения или вышел из него, поскольку событие генерируется в обоих направлениях. Кнопка Перейти в полноэкранный режим также привязывает обработчик .catch к обещанию, возвращаемому функцией requestFullscreen(), поскольку отказ сообщается как отклоненное обещание, так и как событие ошибки.

export default function App() {
  const box = useRef(null);
  const [log, setLog] = useState([]);
  const add = line => setLog(l => [...l, line]);  return (
    <div
      ref={box}
      className="stage"
      onFullscreenChange={() => add(document.fullscreenElement ? 'entered fullscreen' : 'left fullscreen')}
      onFullscreenError={e => add('fullscreen error: ' + e.type)}
    >
      <button onClick={() => box.current.requestFullscreen().catch(err => add('rejected: ' + err.message))}>
        Go fullscreen
      </button>{' '}
      <button onClick={() => document.fullscreenElement && document.exitFullscreen()}>Exit</button>
      <pre className="log">{log.join('\n') || 'no events yet'}</pre>
    </div>
  );
}

Шаблон стиля использует псевдокласс :fullscreen для центрирования контента, когда экран полностью занимает элемент в режиме полноэкранного отображения, а также функцию light-dark() для выбора фона, соответствующего цветовой схеме.

.stage { padding: 12px; border-radius: 10px; background: light-dark(#f1f5f9, #262a33); }
.stage:fullscreen { display: grid; place-content: center; }
.log { margin: 10px 0 0; font-size: 14px; }

onFullscreenError срабатывает, когда браузер отклоняет запрос. Два наиболее распространенных причина — вызов requestFullscreen() вне действия пользователя и выполнение кода внутри <iframe>, у которого отсутствует параметр allow="fullscreen". Если вы встраиваете плеер на странице стороннего сайта и режим полноэкранного просмотра не работает, первым делом следует проверить отсутствие такого разрешения для iframe, а новое свойство позволяет удобно записывать соответствующие сообщения.

Переходы больше не ждут друг друга

В записи изменений говорится, что теперь каждый переход отрисовывается отдельно, вместо того чтобы объединяться с другими в одну общую отрисовку, что предотвращает замедление работы из-за медленного перехода. Это изменение (#37290) вводит флаг enableParallelTransitions.

До версии 19.3, если при этом, как React ещё отрисовывал первое обновление, поступало второе обновление методом startTransition, они сливались вместе. Процесс отрисовки возобновлялся с применением обоих обновлений, и ни одно из них не могло быть выполнено до завершения этой совместной отрисовки. Таким образом, быстрая и недорогая транзиция могла оказаться заложницей дорогостоящей транзиции, с которой у неё не было никакого отношения. Теперь каждая транзиция является самостоятельной единицей работы. Если вы хотите понять, как React внутренне отслеживает эти единицы, наша статья о том, как React использует механизмы lanes для кодирования приоритета обновлений рассматривает основную модель этого процесса.

Способ написания кода для переходов остается прежним, и правила упорядочивания в React по-прежнему действуют: более поздняя обновление никогда не будет сохраняться вместе со старым состоянием. Однако одно предположение теперь становится неверным — обновления, выполненные с помощью двух разных вызовов startTransition, могут поступать в разные кадры. Если несколько обновлений должны появиться одновременно, поместите их в один вызов startTransition.

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

Формы: onReset после действия и новая формулировка ошибок

После завершения обработки действия формы React автоматически сбрасывает её. Ранее это происходило при отключенной системе событий, из-за чего событие reset оставалось незамеченным. Обработчик onReset, предназначенный для сброса управляемого состояния, так и не запускался, в результате чего управляемые поля продолжали отображать свои предыдущие значения (#35176, что решает проблему #33630). В версии 19.3 событие onReset теперь срабатывает и при автоматическом сбросе. Если вы обходили эту ошибку, прослушивая нативное событие reset, вы можете вернуться к использованию этого свойства и удалить соответствующий эффект.

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

Изменения во время разработки: строгий режим, use() и быстрая перезагрузка

Три изменения касаются только версий для разработки, но каждое из них может привести к неожиданным последствиям после обновления.

  • Строгий режим во время гидратации. Теперь эффекты вызываются дважды во время гидратации в строгом режиме (#35961), что соответствует поведению корневых элементов, отрендеренных на стороне клиента, с момента появления React 18. Если приложение, отрендеренное на сервере, казалось безопасным в строгом режиме только потому, что гидратация пропускала второй вызов, это изменение выявит такую проблему. Исправление заключается в том, чтобы сделать эффекты идемпотентными и обеспечить их корректную очистку.
  • Предупреждение относительно условного использования use(). Допускается вызов use условно. Проблема возникает, когда компонент приостанавливает выполнение через use(), а при повторной попытке переходит на ветку, где use() вообще не вызывается. В результате граница Suspense решается по неверной причине. Теперь React выдает предупреждение в режиме разработки, объясняющее, что библиотека приостановила работу через use() в предыдущем отрисовывании, но пропустила его вызов после завершения операции, и что это неправильное использование хука. В формулировке указывается, что причиной часто является библиотека для работы с данными, а не собственный компонент (#37104, #37203, #37491).
  • Исправления функции быстрого обновления. Функция горячей замены теперь справляется со трьмя случаями: компонентом, загруженным с помощью lazy(), изменённой функцией сравнения memo() и редактированием, превращающим компонент в другой тип (#36965, #36964, #36963, #36950). Если ранее такие изменения иногда приводили к полной перезагрузке страницы, причинами были именно они.
  • Меньшие изменения в DOM

    • Обновления, связанные с resize, теперь группируются до следующей кадровой процедуры (#35117), так же как это уже делается с событиями прокрутки. Состояние, установленное в обработчике resize, больше не сбрасывается раньше времени только из-за клика, произошедшего в середине кадра.
    • maskType теперь распознаётся как свойство SVG (#35921).
    • Теперь ресурсы модулей могут содержать указатель приоритета: передайте параметр fetchPriority в функции ReactDOM.preloadModule или ReactDOM.preinitModule, и React отобразит его в виде атрибута fetchpriority (#36835). Это позволяет плагинам для сборки RSC отмечать части клиентских компонентов как имеющие низкий приоритет. Функции preload и preinit, не связанные с модулями, уже поддерживали эту возможность.
    • Атрибут credentialless теперь рассматривается как логический атрибут в элементе <iframe> (#36148), и React предлагает правильную форму написания, если использовать credentialLess (#36322).
    • React больше не переопределяет значение innerHTML, если оно не изменилось (#36949). Повторная отрисовка с использованием того же значения dangerouslySetInnerHTML больше не заставляет браузер снова парсить маркировку, что также сохраняет любое состояние DOM внутри неё.
    • defaultValue для полей вида type="number" теперь ведет себя так же, как и для других типов полей (#36980).
    • Событие toggle копирует свойство source для подвсплывающих окон (#37389).

    Отрисовка на сервере и серверные компоненты

    • Ошибки, возникающие на сервере, теперь доходят до клиента вместе с их cause (#35810), причем объект AggregateError сохраняет свой массив errors (#36156). Механизмы обработки ошибок и код логирования, анализирующие цепочку ошибок, видят одинаковую структуру с обеих сторон.
    • Теперь серверные компоненты могут отрисовывать <Activity>, поскольку Flight его понимает (#34697).
    • react-dom/server больше не заедает в среде Deno (#35235), а точка входа Bun теперь включает все API Node (#34193).

    Исправления багов, симптомы которых вы могли заметить

    Полный список довольно длинный. Это те записи, которые с наибольшей вероятностью соответствуют багу, с которым вы уже сталкивались.

    • useDeferredValue может застрять на устаревшем значении (#36134). Устаревшее значение, которое иногда так и не обновлялось, было багом React, а не ошибкой в вашем коде.
    • Изменения контекста не всегда доходили до механизмов восстановления Suspense (#36160) или до поддеревья, находящегося за границей и остававшегося в режиме ожидания (#35839).
    • useEffectEvent внутри компонентов forwardRef и memo считывает устаревшие значения (#34831).
    • Когда дерево <Activity> было скрыто, useSyncExternalStore упускал изменения в хранилище данных (#36947). Кроме того, скрытое дерево прекращает передачу своего элемента <title> вверх (#34983), не показывает возникающие ошибки в видимом интерфейсе (#35074) и больше не отображает содержимое, которое оно рендерит в порталах (#35091).
  • Состояние формы сбрасывалось при изменении несвязанного состояния (#34075).
  • Приложения, использующие нонсы CSP, получали ложное предупреждение о несоответствии при гидратации атрибутов nonce (#37030).
  • Была исправлена обработка фокуса для делегированных элементов и элементов, уже находившихся в фокусе (#36010).
  • Были устранены две проблемы с <ViewTransition>: одна в Mobile Safari (#35337), другая при использовании с SuspenseList (#35520). Это важно, если вы пробовали экспериментальные версии.
  • В части Server Components функция decodeReplyFromBusboy теряла элементы FormData (#36468), глубокие асинхронные цепочки могли привести к переполнению стека (#35612), а информация для отладки могла расти экспоненциально, что приводило к возникновению ошибки RangeError (#37481).
  • Полный список пул-реквестов доступен по ссылке в CHANGELOG React.

    Сколько байт стоит обновление

    Вскоре после выпуска в обсуждении на r/reactjs появился комментарий о росте размера файла react-dom-client.production.js с 523,5 kB до 610,5 kB, и был задан вопрос о причине этого. Сравнив версии 19.2.0 и 19.3.0, опубликованные на unpkg, получаем следующие цифры:

    • cjs/react-dom-client.production.js в несжатом виде: 536,016 B в версии 19.2.0 против 625,168 B в версии 19.3.0, что на 89 kB (16,6 %) больше.
    • Тот же файл в формате gzip с уровнем сжатия 9: 94,7 kB против 110,5 kB, что на 15,7 kB больше.
    • cjs/react-dom-server.node.production.js в несжатом виде: 277,046 B против 300,544 B, что на 23,5 kB больше.
    • cjs/react.production.js в несжатом виде: 17,217 B против 18,040 B, что на 0,8 kB больше.

    Цифры в комментариях на Reddit указывают на одинаковые размеры данных, выраженные в KiB. Причина проста: элементы View Transitions, Fragment Refs, browser() и кодовый путь Trusted Types раньше находились за фич-флагами, которые исключали их из стабильной версии. В версии 19.3 они включены в стабильную версию. По сети разница составляет примерно 16 kB в заархивированном виде, что сопоставимо с набором икон среднего размера.

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

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

    • Читайте значение e.submitter непосредственно в обработчиках onSubmit; параметры действия формы уже содержат значения name и value кнопки.
    • Замените ручные обработчики события fullscreenchange на onFullscreenChange и onFullscreenError, а также обработайте отклонённое обещание, возвращаемое функцией requestFullscreen().
    • Теперь отдельные вызовы функции startTransition могут выполняться в разных фреймах, поэтому группируйте обновления, которые должны отобразиться вместе.
    • Функция onReset теперь выполняется только после автоматического сброса действия, а режим строгой проверки приводит к двукратному вызову эффектов во время их активации, поэтому перепроверьте процедуру очистки эффектов.
    • Исправлены несколько давно существующих ошибок, включая проблему с застрявшим значением useDeferredValue и устаревшими значениями useEffectEvent; проверьте, можно ли устранить аналогичные проблемы в вашем коде с помощью временных решений.
  • Бюджет примерно 16 кБ в сжатом виде дополнительно для react-dom на стороне клиента; ранее отмеченные как дополнительные функции теперь включены в стабильные версии.