Головна / Статті

React 19.3 – більше, ніж просто заголовки: submitter, властивості Fullscreen та розмір пакету

Практичний огляд менш значущих змін у 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 елемента, який надсилає дані, корисне лише тоді, коли ви дійсно встановлюєте value (а зазвичай і name) для кожної кнопки. Кнопка без цих значень створює порожній рядок, що легко можна сплутати з відсутністю функції.

Події повного екрану як пропси першого класу

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

У демонстрації потрібен ref для елемента, який переходить у режим повного екрану, та стан для запису подій.

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 кодує пріоритет оновлень розглядає основну модель.

Спосіб написання функцій Transitions залишається незмінним, і правила порядку виконання в React все ще діють: оновлення, яке відбувається пізніше, ніколи не буде застосоване разом із старим станом. Однак одне припущення тепер виявляється хибним – оновлення, отримані через два різних виклики startTransition, можуть виконуватися у різних фреймах. Якщо кілька оновлень мають виконатися одночасно, помістіть їх у один виклик startTransition.

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

Форми: onReset після дії та новий формулювання помилок

Коли завершується виконання дії форми, React автоматично скидає її. До цього моменту він робив це з вимкненою системою подій, через що подія reset залишалась непоміченою. Обробник onReset, призначений для очищення керованого стану, ніколи не виконувався, і керовані поля продовжували відображати свої попередні значення (#35176, що виправляє проблему #33630). У версії 19.3 подія onReset також викликається під час автоматичного скидання. Якщо ви обійшли цю проблему, слухаючи вбудовану подію reset, ви можете повернутися до цього параметра та видалити відповідний ефект.

Схожа косметична зміна: повідомлення про помилки від useActionState тепер посилаються на „стан дії“, а не на „стан форми“ (#35790). Функціонал залишився незмінним, але пошук у журналах, правила попереджень чи тести, які відповідають старому формулюванню, потребують оновлення.

Зміни під час розробки: Strict Mode, use() та Fast Refresh

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

  • Strict Mode під час гідратації. Тепер ефекти викликаються двічі під час гідратації в режимі Strict Mode (#35961), що відповідає поведінці кореневих елементів, які обробляються на клієнті, з часів React 18. Якщо додаток, який обробляється на сервері, здавався безпечним у режимі Strict Mode лише через те, що гідратація пропускала другий виклик, це тепер розкриє його проблеми. Виправлення є звичайним: зробити ефекти ідемпотентними та забезпечити їх належне очищення.
  • Попередження щодо умовного використання use(). Дозволяється викликати use умовно. Проблемна ситуація виникає, коли компонент був призупинений через use(), а під час повторної спроби обрав гілку, де use() взагалі не викликався. У результаті межа Suspense розв’язується з неправильної причини. Тепер React виводить попередження для розробників, пояснюючи, що бібліотека, яка була призупинена через use() під час попереднього відображення, пропустила його після завершення роботи, і що це є неправильним використанням гука. У формулюванні натякається, що причиною часто є бібліотека даних, а не власний компонент (#37104, #37203, #37491).
  • Виправлення для швидкого оновлення. Функція гарячого завантаження тепер справляється з трьома випадками: компонент, завантажений за допомогою lazy(), змінена функція порівняння memo(), а також зміни, які перетворюють компонент на інший тип (#36965, #36964, #36963, #36950). Якщо раніше ці зміни іноді змушували повністю перезавантажувати сторінку, то причиною були саме вони.
  • Більш незначні зміни в DOM

    • Оновлення через resize тепер групуються до наступного кадру (#35117), так само, як це вже робиться з подіями прокрутки. Стан, встановлений у слухачі зміни розміру, більше не виводиться раніше лише через те, що клік відбувається посеред кадру.
    • 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).
  • Повний список pull requests доступний у React CHANGELOG.

    Скільки байтів коштує оновлення

    Невдовзі після випуску у форумі 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 нарешті виконується після автоматичного скидання дії, а режим Strict Mode тепер подвійно викликає ефекти під час гідратації, тому перевірте очищення ефектів.
    • Виправлено кілька давніх багів, включаючи проблему з useDeferredValue та застарілі значення useEffectEvent; перевірте, чи можна використати якісь обхідні заходи у вашому коді.
  • Бюджет приблизно 16 kB у стиснутому вигляді додатково для react-dom на клієнті; функції, які раніше були позначені як додаткові, тепер включені до стабільних версій.