React 19.3: что за пределами заголовков — отправитель данных, атрибуты полноэкранного режима и размер пакета
Практический обзор менее значительных изменений в React 19.3: свойство submitter, параметры события fullscreen, параллельные переходы, метод onReset, важные исправления багов и реальное увеличение размера.
Заметки об обновлении обычно выделяют несколько ключевых нововведений, и 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 & 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 & 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).
nonce (#37030).<ViewTransition>: одна в Mobile Safari (#35337), другая при использовании с SuspenseList (#35520). Это важно, если вы пробовали экспериментальные версии.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; проверьте, можно ли устранить аналогичные проблемы в вашем коде с помощью временных решений.
react-dom на стороне клиента; ранее отмеченные как дополнительные функции теперь включены в стабильные версии.