React 19.3 Poza nagłówkami: submitter, Fullscreen Props i rozmiar pliku
Praktyczny przegląd mniejszych zmian w React 19.3: właściwość submitter, atrybuty zdarzenia fullscreen, równoległe przejścia, funkcja onReset, istotne poprawki błędów oraz rzeczywisty wzrost rozmiaru.
Notatki wydawnicze zazwyczaj podkreślają kilka najważniejszych funkcji, a React 19.3 nie jest wyjątkiem. Poniżej znajduje się lista „innych istotnych zmian” oraz długa sekcja z poprawkami błędów, z których większość to pojedyncze linijki łatwe do pominięcia. Jednak niektóre z tych linijek zmieniają sposób pisania komponentów, wyjaśniają błędy, z którymi być może już sobie poradziliśmy, lub wpływają na to, co wysyłamy użytkownikom. Ten przewodnik omawia je mniej więcej w kolejności ich znaczenia dla Ciebie i kończy się precyzyjną odpowiedzią na pytanie, które zadaje każdy po aktualizacji: o ile większy stał się plik instalacyjny?
Wiedza o tym, który przycisk wysłał formularz
Formularz z dwoma przyciskami wysyłania, na przykład Save i Save & close, musi wiedzieć, który z nich nacisnął użytkownik. Przeglądarki od dawna odpowiadają na to pytanie: wbudowany SubmitEvent udostępnia właściwość submitter, która wskazuje na przycisk, który spowodował wysłanie danych. Jednak syntetyczny event w React nie przekazywał tej informacji. Powszechnymi rozwiązaniami były odczytywanie wartości e.nativeEvent.submitter lub, mniej elegancko, zapisywanie ostatnio klikniętego przycisku w stanie i liczenie na to, że żadne wysłanie danych przez klawiaturę nie umknie uwadze. React 19.3 dodaje właściwość submitter do syntetycznego eventu wysyłania (#35590).
Najprostszym sposobem na zobaczenie różnicy jest uruchomienie tego samego formularza w obu wersjach. Demo zaczyna się od importów; import przestrzeni nazw jest tam tylko po to, aby komponent mógł wyświetlić wartość React.version.
import { useState } from 'react';
import * as React from 'react';
Komponent blokuje standardową nawigację, a następnie zapisuje wersję React oraz wartość e.submitter do stanu. Każdy przycisk ma własną wartość value, co dokładnie odpowiada warunkom, na których decyduje się w prawdziwym kodzie.
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>
);
}
Jedna zasada stylizuje linię wyjściową za pomocą czcionki monospaced.
.msg { margin: 10px 0 0; font-family: ui-monospace, monospace; }
Ta sama forma w wersji 19.3
Wersja 19.3 demonstracji jest identyczna, co jest właśnie celem: zmienia się tylko wersja React. Importy pozostają takie same.
import { useState } from 'react';
import * as React from 'react';
Tak samo jest z komponentem.
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>
);
}
I tak samo jest ze skryptem stylów.
.msg { margin: 10px 0 0; font-family: ui-monospace, monospace; }
W wersji 19.2 komunikat pokazuje undefined, ponieważ syntetyczne zdarzenie nie posiada takiego pola. W wersji 19.3 pokazuje się save lub save-close, w zależności od przycisku. Należy pamiętać, który ścieżka kodowa jest tym dotknięty. Jeśli wysyłasz dane za pomocą działań formularza zamiast obsługownika onSubmit, React już odczytywał element wysyłający podczas tworzenia obiektu FormData (#29028), więc para name i value naciskanego przycisku była już obecna w danych akcji. Nowa właściwość ma znaczenie dla obsługowników zdarzeń.
Praktyczna uwaga: wartość value elementu wysyłającego jest przydatna tylko wtedy, gdy faktycznie ustawisz value (a zazwyczaj także name) dla każdego przycisku. Przycisk bez tych wartości generuje pustą ścieżkę, co łatwo można pomylić z brakiem funkcjonalności.
Zdarzenia pełnoekranowe jako właściwości pierwszej klasy
API Fullscreen wysyła wydarzenia fullscreenchange i fullscreenerror do danego elementu. React nie rozpoznał tych nazw wydarzeń, więc komponenty takie jak odtwarzacze wideo musiały ręcznie dodać słuchacze za pomocą addEventListener wewnątrz efektu i pamiętać o ich usunięciu podczas czyszczenia. React 19.3 dodaje onFullscreenChange i onFullscreenError jako zwykłe właściwości (#34621).
Demo wymaga referencji do elementu, który przechodzi w tryb pełnoekranowy, oraz pola stanu do przechowywania logu wydarzeń.
import { useRef, useState } from 'react';
Zauważ, że oba obsługiwanie zdarzeń znajdują się bezpośrednio na elemencie div. Obsługa zmian sprawdza wartość document.fullscreenElement, aby określić, czy element właśnie wszedł w tryb pełnoekranowy lub z niego wyszedł, ponieważ samo zdarzenie jest wyzwalane w obu kierunkach. Przycisk Przejdź w tryb pełnoekranowy dodaje również element .catch do obietnicy zwróconej przez funkcję requestFullscreen(), ponieważ odrzucenie jest zgłaszane zarówno jako odrzucona obietnica, jak i jako zdarzenie błędu.
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>
);
}
Skrypt stylizacji wykorzystuje pseudo-klasę :fullscreen, aby uśrodkować treść, gdy ekran jest wypełniony elementem pełnoekranowym, a funkcję light-dark(), aby wybrać tło pasujące do ustalonej palety kolorów.
.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 jest wyzwalany, gdy przeglądarka odrzuca żądanie. Dwie najczęstsze przyczyny to wywołanie requestFullscreen() poza gestem użytkownika oraz uruchomienie go wewnątrz <iframe>, który nie ma atrybutu allow="fullscreen". Jeśli wstawiasz odtwarzacz na stronie third-party, a tryb pełnoekranowy nie działa, pierwszą rzeczą do sprawdzenia jest brakująca zezwolenie dla iframe, a nowa właściwość umożliwia jej rejestrację w wygodny sposób.
Przejścia nie czekają już na siebie
Zapis w changelogu mówi, że każde przejście jest teraz renderowane oddzielnie, zamiast łączyć się z innymi w jeden proces renderowania, dzięki czemu wolne przejście nie może blokować tych niespowiązanych. Ta zmiana (#37290) umożliwia użycie flagi enableParallelTransitions.
Przed wersją 19.3, jeśli podczas renderowania pierwszej aktualizacji przez React nadeszła druga aktualizacja typu startTransition, obie były łączone. Wtedy trwający render rozpoczynał się od nowa z zastosowaniem obu aktualizacji, a żadna z nich nie mogła zostać zrealizowana, dopóki cały ten połączony proces renderowania nie został zakończony. Dlatego szybka i tania transycja mogła zostać „zakładniczką” drogiej transycji, z którą nie miała nic wspólnego. Obecnie każda transycja jest odrębną jednostką pracy. Jeśli chcesz zrozumieć, w jaki sposób React monitoruje te jednostki wewnętrznie, naszy artykuł na temat sposobu, w jaki React koduje priorytet aktualizacji omawia podstawowy model tego mechanizmu.
Sposób zapisywania przejść nie zmienia się, a zasady uporządkowania w React nadal obowiązują: późniejsza aktualizacja nigdy nie zostanie zapisana razem ze starszym stanem. Jednak jedno założenie okazuje się błędne – aktualizacje wywołane poprzez dwa różne wywołania startTransition mogą teraz trafić do różnych ram. Jeśli kilka aktualizacji musi pojawić się razem, umieść je w jednym wywołaniu startTransition.
Nie oczekuj, że zobaczysz to w przykładzie demonstracyjnym. Próba odtworzenia tego z powolnym drzewem składającym się z 300 komponentów oraz błahą szybką aktualizacją zaplanowaną na 50 ms później pokazała, że szybka aktualizacja zostaje zapisana po powolnej zarówno w wersjach 19.2, jak i 19.3. Ulepszenie to dotyczy poziomu planerów i jest widoczne w dużych aplikacjach pod rzeczywistym obciążeniem, a nie w małych demonstracjach.
Formularze: onReset po akcji oraz nowe sformułowania błędów
Gdy zakończy się działanie formularza, React automatycznie resetuje go. Do tej pory dokonywał tego resetu z wyłączonym systemem zdarzeń, co oznaczało, że zdarzenie reset było ignorowane. Obsługa onReset, mająca na celu usunięcie sterowanego stanu, nigdy nie była wykonywana, a pola wejściowe typu controlled nadal pokazywały swoje poprzednie wartości (#35176, co naprawia problem #33630). W wersji 19.3 zdarzenie onReset jest również wyzwalane podczas automatycznego resetu. Jeśli obejścia tego błędu realizowaliście poprzez samodzielne obserwowanie natywnego zdarzenia reset, możecie wrócić do tej właściwości i usunąć odpowiedni efekt.
Związana z tym zmiana estetyczna: komunikaty błędowe funkcji useActionState odnoszą się teraz do „stanu akcji”, a nie do „stanu formularza” (#35790). Zachowanie pozostaje niezmienne, ale wyszukiwania w logach, reguły alertów lub testy odpowiadające starej sformułowaniu wymagają aktualizacji.
Zmiany w czasie rozwoju: Strict Mode, use() i Fast Refresh
Trzy zmiany dotyczą wyłącznie wersji przeznaczonych do rozwoju, ale każda z nich może ujawnić coś zaskakującego po aktualizacji.
- Strict Mode podczas hydratacji. Efekty są teraz wywoływane dwukrotnie podczas hydratacji w trybie Strict Mode (#35961), co odpowiada zachowaniu korzeni renderowanych po stronie klienta od wersji React 18. Jeśli aplikacja renderowana na serwerze wydawała się bezpieczna w trybie Strict Mode tylko dlatego, że hydratacja pomijała drugie wywołanie, to teraz to zostanie ujawnione. Rozwiązanie jest standardowe: należy sprawić, by efekty były idempotentne i zapewnić im odpowiednie usuwanie po zakończeniu pracy.
use(). Dozwolone jest warunkowe wywoływanie funkcji use. Problemem jest komponent, który został wstrzymany przy użyciu use(), a następnie podczas ponownej próby skorzystał z gałęzi, która w ogóle nie wywołała use(). W rezultacie granica Suspense rozwiązuje się z niewłaściwego powodu. React teraz wyświetla ostrzeżenie rozwojowe, które wyjaśnia, że biblioteka wstrzymana za pomocą use() podczas wcześniejszego renderowania pominęła wywołanie use() po zakończeniu swojej pracy, co stanowi niewłaściwe użycie tego hooka. Sformułowanie wskazuje, że przyczyną jest często biblioteka do obsługi danych, a nie własny komponent (#37104, #37203, #37491).lazy(), zmienioną funkcją porównania memo() oraz edycją, która przekształca komponent w inny typ (#36965, #36964, #36963, #36950). Jeśli te edycje czasami powodowały pełne odświeżenie strony, to były one przyczyną.Mniejsze zmiany w DOM
- Aktualizacje
resizesą teraz grupowane w jedną transakcję do następnego kadru (#35117), tak samo jak to już jest w przypadku zdarzeń przewijania. Stan ustawiony w słuchaczuresizenie jest już wcześnie wysyłany tylko dlatego, że w trakcie kadru następuje kliknięcie. maskTypejest teraz rozpoznawany jako właściwość SVG (#35921).- Zasoby modułów mogą teraz zawierać wskazówkę dotyczącą priorytetu: przekaż
fetchPrioritydoReactDOM.preloadModulelubReactDOM.preinitModule, a React wyświetli go jako atrybutfetchpriority(#36835). Dzięki temu plugin do pakowania RSC może oznaczyć fragmenty komponentów klienckich jako o niskim priorytecie. Funkcjepreloadipreinit, które nie są modułami, już to obsługują. credentiallessjest traktowane jako atrybut logiczny w elementach<iframe>(#36148), a React sugeruje prawidłową pisownię, jeśli użyjeszcredentialLess(#36322).- React nie przypisuje już na nowo wartości do
innerHTML, gdy ta się nie zmieniła (#36949). Ponowne renderowanie za pomocą identycznegodangerouslySetInnerHTMLnie zmusza już przeglądarki do ponownego analizowania kodu, co chroni również stan DOM w jego wnętrzu. defaultValuew polach typutype="number"teraz zachowuje się tak samo jak w innych typach pól (#36980).- Zdarzenie
togglekopiuje właściwośćsourcedla elementów typu popover (#37389).
Renderyzacja na serwerze i komponenty serwerowe
- Błędy wywoływane na serwerze docierają teraz do klienta wraz ze swoją wartością
cause(#35810), a obiektAggregateErrorzachowuje swój tablicęerrors(#36156). Elementy obsługujące błędy oraz kod logowania, które analizują łańcuch błędów, widzą taką samą strukturę po obu stronach. - Komponenty serwerowe mogą teraz renderować element
<Activity>, ponieważ Flight go rozumie (#34697). react-dom/servernie zawiesza się już w środowisku Deno (#35235), a punkt wejścia Bun obejmuje teraz wszystkie API Node’a (#34193).
Poprawki błędów, których objawy mogliście zauważyć
Pełna lista jest długa. To są wpisy, które najprawdopodobniej odpowiadają błędowi, który już próbowałeś naprawić.
useDeferredValuemoże utknąć przy przestarzałej wartości (#36134). Wartość odroczona, która czasami w ogóle nie jest aktualizowana, była błędem React, a nie błędem w twoim kodzie.- Zmiany kontekstu nie zawsze docierały do mechanizmów awaryjnych Suspense (#36160) ani do poddrzewa za granicą, które nadal było w stanie zawieszenia (#35839).
useEffectEventw komponentachforwardRefimemoodczytuje przestarzałe wartości (#34831).- Gdy drzewo
<Activity>było ukryte,useSyncExternalStoreprzeoczało modyfikacje w pamięci przechowywania (#36947). Ponadto ukryte drzewo przestaje przenosić swoje<title>(#34983), nie pokazuje błędów, które wystąpiły, w widocznym interfejsie (#35074) i nie wyświetla już tego, co renderuje w portalach (#35091).
nonce (#37030).<ViewTransition> zostały naprawione: jedna w Mobile Safari (#35337) oraz druga przy użyciu SuspenseList (#35520). Są one istotne, jeśli próbowałeś eksperymentalnych wersji.decodeReplyFromBusboy traciła elementy z FormData (#36468), głębokie łańcuchy asynchroniczne mogły przepełnić stos (#35612), a informacje diagnostyczne mogły rosnąć wykładniczo, co prowadziło do błędu RangeError (#37481).Pełny zestaw prośb o zmiany znajduje się w dokumencie React CHANGELOG.
Ile bajtów kosztuje aktualizacja
Niedługo po wydaniu w dyskusji na r/reactjs pojawił się komentarz wskazujący na wzrost rozmiaru pliku react-dom-client.production.js z 523,5 kB do 610,5 kB i pytający o przyczynę. Porównanie wersji 19.2.0 i 19.3.0 opublikowanych na unpkg pokazuje następujące wartości:
cjs/react-dom-client.production.jsw formie surowej: 536 016 B w wersji 19.2.0 w porównaniu z 625 168 B w wersji 19.3.0, co stanowi wzrost o 89 kB (16,6 %).- Ten sam plik po kompresji gzip na poziomie 9: 94,7 kB w porównaniu z 110,5 kB, czyli wzrost o 15,7 kB.
cjs/react-dom-server.node.production.jsw formie surowej: 277 046 B w porównaniu z 300 544 B, co oznacza wzrost o 23,5 kB.cjs/react.production.jsw formie surowej: 17 217 B w porównaniu z 18 040 B, czyli wzrost o 0,8 kB.
Liczby w komentarzu na Reddit są wyrażone w tych samych jednostkach, ale w formie KiB. Przyczyna jest prosta: View Transitions, Fragment Refs, browser() oraz ścieżka kodowa Trusted Types były ukryte za flagami funkcjonalnymi, które usunęły je z wersji stabilnej. W wersji 19.3 zostały one ponownie włączone. Przez sieć różnica wynosi około 16 kB po stłumieniu, co jest porównywalne z zestawem ikon średniej wielkości.
To rzeczywisty koszt, który warto uwzględnić w budżecie wydajnościowym, ale nie stanowi regresu w zachowaniu Reacta. Jeśli dodatkowe bajty mają znaczenie dla twojej aplikacji, skuteczniejszymi rozwiązaniami są zazwyczaj dzielenie kodu, redukcja własnych zależności oraz odkładanie na później implementacji niekrytycznych komponentów klienta, zamiast wstrzymywać aktualizację.
Główne wnioski
- Czytaj
e.submitterbezpośrednio w obsługiwaczachonSubmit; akcje formy otrzymały jużnameivalueprzycisku. - Zastąp ręczne słuchacze
fullscreenchangemetodamionFullscreenChangeionFullscreenError, a także obsłuż odrzuconą obietnicę zrequestFullscreen(). - Oddzielne wywołania
startTransitionmogą teraz zostać zrealizowane w różnych ramkach, więc grupuj aktualizacje, które muszą pojawić się razem. onResetjest w końcu wykonywany po automatycznym sformatowaniu akcji, a tryb Strict Mode teraz dwukrotnie uruchamia efekty podczas hydratacji, więc ponownie sprawdź czyszczenie efektów.- Liczne długo istniejące błędy, w tym zablokowane wartości
useDeferredValueoraz przestarzałe wartościuseEffectEvent, zostały naprawione; sprawdź, czy jakiekolwiek rozwiązania workaround w twoim kodzie mogą zostać usunięte.
react-dom na stronie klienta; funkcje wcześniej oznaczone jako dodatkowe są teraz zawarte w wersjach stabilnych.Literatura pokrewna
- Poza rozmiarem pakietu: odkrywanie tego, co naprawdę spowalnia twoją aplikację webową — Dlaczego redukcja liczby kilobajtów rzadko naprawia wolną aplikację oraz jak śledzić rzeczywisty czas oczekiwania na serwerach, w procesach typu waterfall, podczas hydratacji, w skryptach i obrazach od dostawców zewnętrznych.
- Skracanie React Prop-Drilling i God Components do optymalnych rozmiarów — Poznaj siedem konkretnych wzorców refaktoryzacji umożliwiających rozbicie rozbudowanych komponentów React poprzez izolację stanu, pobierania danych, uprawnień i logiki ładowania, zamiast jedynie dzielenia plików.
- Fragment Refs w React 19.3: Zachowanie DOM dla grup elementów braci, bez opakowania — Dowiedz się, jak ref w Fragment pozwala uzyskać instancję FragmentInstance do obsługi wydarzeń, skupienia uwagi, obserwacji i pomiarów pomiędzy elementami braci oraz jakie są jego ograniczenia.