Strona główna / Artykuły

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.

Wydawca: REACTAPP.TOP11 min czytania2490 słów

Źródła: Everything Else in React 19.3: submitter, Fullscreen Events, Independent Transitions, the Bug Fixes and the Bundle Cost

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 &amp; 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 &amp; 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.
  • Ostrzeżenie dotyczące warunkowego użycia 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).
  • Poprawki dotyczące szybkiego odświeżania. Hot reloading teraz radzi sobie z trzema przypadkami: komponentem załadowanym za pomocą 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 resize są teraz grupowane w jedną transakcję do następnego kadru (#35117), tak samo jak to już jest w przypadku zdarzeń przewijania. Stan ustawiony w słuchaczu resize nie jest już wcześnie wysyłany tylko dlatego, że w trakcie kadru następuje kliknięcie.
    • maskType jest teraz rozpoznawany jako właściwość SVG (#35921).
    • Zasoby modułów mogą teraz zawierać wskazówkę dotyczącą priorytetu: przekaż fetchPriority do ReactDOM.preloadModule lub ReactDOM.preinitModule, a React wyświetli go jako atrybut fetchpriority (#36835). Dzięki temu plugin do pakowania RSC może oznaczyć fragmenty komponentów klienckich jako o niskim priorytecie. Funkcje preload i preinit, które nie są modułami, już to obsługują.
    • credentialless jest traktowane jako atrybut logiczny w elementach <iframe> (#36148), a React sugeruje prawidłową pisownię, jeśli użyjesz credentialLess (#36322).
    • React nie przypisuje już na nowo wartości do innerHTML, gdy ta się nie zmieniła (#36949). Ponowne renderowanie za pomocą identycznego dangerouslySetInnerHTML nie zmusza już przeglądarki do ponownego analizowania kodu, co chroni również stan DOM w jego wnętrzu.
    • defaultValue w polach typu type="number" teraz zachowuje się tak samo jak w innych typach pól (#36980).
    • Zdarzenie toggle kopiuje właściwość source dla 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 obiekt AggregateError zachowuje 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/server nie 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ć.

    • useDeferredValue moż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).
    • useEffectEvent w komponentach forwardRef i memo odczytuje przestarzałe wartości (#34831).
    • Gdy drzewo <Activity> było ukryte, useSyncExternalStore przeoczał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).
  • Stan formularza został sformatowany, gdy zmienił się niepowiązany stan (#34075).
  • Aplikacje używające tokenów CSP otrzymywały fałszywe ostrzeżenia o niezgodności hydratacji dotyczące atrybutów nonce (#37030).
  • Błędy w obsłudze fokusa zostały poprawione dla elementów delegowanych oraz tych, które już miały fokus (#36010).
  • Liczne awarie związane z <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.
  • W przypadku Server Components funkcja 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.js w 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.js w formie surowej: 277 046 B w porównaniu z 300 544 B, co oznacza wzrost o 23,5 kB.
    • cjs/react.production.js w 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.submitter bezpośrednio w obsługiwaczach onSubmit; akcje formy otrzymały już name i value przycisku.
    • Zastąp ręczne słuchacze fullscreenchange metodami onFullscreenChange i onFullscreenError, a także obsłuż odrzuconą obietnicę z requestFullscreen().
    • Oddzielne wywołania startTransition mogą teraz zostać zrealizowane w różnych ramkach, więc grupuj aktualizacje, które muszą pojawić się razem.
    • onReset jest 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 useDeferredValue oraz przestarzałe wartości useEffectEvent, zostały naprawione; sprawdź, czy jakiekolwiek rozwiązania workaround w twoim kodzie mogą zostać usunięte.
  • Budżet około 16 kB po stłumieniu dla react-dom na stronie klienta; funkcje wcześniej oznaczone jako dodatkowe są teraz zawarte w wersjach stabilnych.
  • Literatura pokrewna