Miniony tydzień zdecydowanie zdominował release React 18, który przyniósł ogrom nowych funkcjonalności. Dlatego tego tygodniowa edycja naszego przeglądu w całości poświęcona będzie właśnie bibliotece od Facebooka (ekhem ekhem… Mety)
Wreszcie się doczekaliśmy! Po prawie roku od pojawienia się wersji alpha i po 5 miesiącach od wydania pierwszej bety React 18 oficjalnie został opublikowany. W odróżnieniu od poprzedniej wersji Reacta, która z punktu widzenia API nie wprowadzała znaczących nowości, wersja ta wręcz pęka w szwach od nowych funkcjonalności.
W tym momencie warto również zwrócić uwagę, że zespół odpowiedzialny za Reacta nie zdecydował się wskoczyć w tak popularne dzisiaj Release Trainy. React 17 ukazał się 20 października 2020, czyli 17 miesięcy temu. Dla porównania w tym samym czasie ukazały się aż 3 wersje Angulara, a najnowsza w momencie premiery React 17 wersja frameworka od Google nie jest już wspierana. Patrząc na nowości jakie weszły do React 18 trochę żałuję, że Angular nie pracuje w podobny sposób.
Dzisiejsze wydanie naszego przeglądu możecie potraktować jako wydanie specjalne, bo w pełni poświęcone będzie ono React 18. Przygotujcie sobie jakieś dobre przekąski, złapcie coś do picia i zapraszamy na wspólną podróż przez nowości z frameworka od Mety.
Opcjonalny tryb współbieżny (Concurrent Mode)
Tryb współbieżny (Concurrent Mode) stał się swego rodzaju memem w społeczności Reacta. Prace nad tą funkcjonalnością trwały tak długo, że osobiście zacząłem wątpić, czy kiedykolwiek trafi ona na produkcję. React 18 postanowił pozytywnie mnie zaskoczyć i wprowadził opcjonalny tryb współbieżny. Będzie on uruchamiany w momencie skorzystania z jednego z współbieżnych API, zadziała tylko na wybranej części pod-drzewa komponentów i zadziała tylko jeśli root renderowania zostanie stworzony przy użyciu nowego API.
Jeśli nigdy nie słyszeliście o trybie współbieżnym w React to pewnie drapiecie się teraz w głowę próbując połączyć współbieżność z jednowątkowym JavaScriptem. Jak się okazuje współbieżność polega tutaj na możliwości kolejkowania renderów, nadawaniu im priorytetów oraz dodaniu możliwości przerwania renderu w trakcie.
Komentarz
W tym momencie rozróżnić należy renderowanie jakie wykonuje przeglądarka, oraz renderowanie wykonywane przez Reacta. Renderowanie wykonywane przez przeglądarkę było i pozostaje w pełni synchroniczne i nieprzerywalne z poziomu kodu JavaScript. Renderowanie w kontekście Reacta oznacza natomiast aktualizowanie drzewa (lub pod-drzewa) komponentów, budowanie elementów HTML i w ostatnim kroku łączenia stworzonych elementów z drzewem DOM.
Sztandarowym przykładem, kiedy skorzystamy na trybie współbieżnym, są interakcje z polem tekstowym służącym do wyszukiwania elementów w długiej liście. Kiedy użytkownik naciska klawisz na klawiaturze, nawet najmniejsze opóźnienie w aktualizacji pola tekstowego daje mu poczucie, że coś jest nie tak. Natomiast jeśli chodzi o wyniki wyszukiwania, to drobne opóźnienie jest wręcz naturalne. W takiej sytuacji jasno widać, że do czynienia mamy tu z priorytetowym i niepriorytetowym renderem. Ponadto jeśli użytkownik jest w stanie pisać szybciej niż React renderuje komponenty, to renderowanie pośrednich stanów wyszukiwania jest niepożądane. W tym przypadku do gry wchodzi anulowanie renderowania, które w momencie pojawienia się nowszej wersji komponentu przerwie poprzedni render.
Część z was prawdopodobnie zwróci uwagę na fakt, że podobny efekt można było do tej pory osiągnąć odpowiednio wykorzystując funkcję debounce. Różnica pomiędzy stosowanymi obecnie technikami i trybem współbieżnym polega na tym, że obecnie renderowanie odbywa się synchronicznie i w jego trakcie użytkownik nie jest w stanie interaktować ze stroną. W trybie współbieżnym render o niższym priorytecie zostanie przerwany w momencie, kiedy w kolejce pojawi się render o wyższym priorytecie. Dzięki przerwaniu mniej priorytetowego renderu strona powinna sprawiać wrażenie dużo bardziej responsywnej.
Oczywiście na przykładzie z inputem możliwości wykorzystania trybu współbieżnego się nie kończą. Dzięki jego zastosowaniu w przyszłości możliwe będzie na przykład renderowanie poza ekranem, dzięki któremu cachować będzie można już odwiedzone strony lub zawczasu renderować strony, które najprawdopodobniej użytkownik odwiedzi w dalszej kolejności. Dzięki zastosowaniu priorytetów takie akcji nie będą wpływać na responsywność interfejsu, bo będą po prostu odbywać się z niższym priorytetem.
Wystarczy już teorii – przejdźmy do mięska, czyli nowego API. Nisko priorytetowe aktualizacje opakowane muszą zostać w metodę startTransition, do której dostać możemy się poprzez hook useTransition().
// You can play with this code here: https://stackblitz.com/edit/react-anefuq?file=src/App.js
export default function App() {
const [isPending, startTransition] = useTransition();
const [count, setCount] = useState(0);
function handleClick() {
startTransition(() => {
setCount((c) => c + 1);
});
}
const advancedCounter = useMemo(
() => <AdvancedCounter count={count} />,
[count]
);
return (
<>
{/*Immediately after clicking the button */}
{/*div changes opacity until low priority render finishes*/}
<div style={{ opacity: isPending ? 0.8 : 1 }}>
{/*Component below will update only after all priority renders finish.*/}
{/*If user spams increase button only last render will be visible to user.*/}
{advancedCounter}
<button onClick={handleClick}>Increase counter</button>
</div>
</>
);
}
Do API trafił również hook useDeferredValue. Jeśli hook ten wywołany zostanie w priorytetowym renderze to zwróci wartość z poprzedniego renderu. W ten sposób referencja zmiennej nie zmieni się, co pozwoli nam uniknąć nadmiernego renderowania. Kiedy priorytetowe rendery zostaną wykonane, zakolejkowany zostanie niskopriorytetowy render w którym hook zwróci nową wartość i wyrenderowany zostanie poprawny komponent. Pozwoli on więc w znaczący sposób uprościć powyższy przykład.
// You can play with this code here: https://stackblitz.com/edit/react-t6myxk?file=src/App.js
export default function App() {
const [count, setCount] = useState(0);
function handleClick() {
setCount((c) => c + 1);
}
const deferredCount = useDeferredValue(count);
const advancedCounter = useMemo(
() => <AdvancedCounter count={deferredCount} />,
[deferredCount]
);
return (
<>
<div>
{/*This component will update only after all priority renders finish.*/}
{/*If user spams increase button only last render will be visible to user.*/}
{advancedCounter}
<button onClick={handleClick}>Increase counter</button>
</div>
</>
);
}
Starałem się jak mogłem, żeby w przejrzysty sposób wytłumaczyć Wam jak działa współbieżne renderowanie w React. Niestety jest to temat skomplikowany, a twórcy niezbyt chętnie dzielą się szczegółami na temat jego detali implementacyjnych, dlatego wszystkich mających trochę więcej czasu zachęcam do wniknięcia w odnośniki jakie znajdziemy w notatce towarzyszącej wydaniu i spróbowania jak to rozgryźć samemu.
Zainstaluj teraz i czytaj tylko dobre teksty!
Automatic batching
Do tej pory React w większości przypadków był w stanie zoptymalizować ilość renderów przez ich grupowanie. Haczyk polegał na tym, że React był w stanie grupować tylko aktualizacje stanu, które miały miejsce w tzw. React Event Handlers. Jeśli aktualizacje stanu miały miejsce poza tym kontekstem (a miało to miejsce na przykład wewnątrz setTimeout), to React renderował drzewo komponentów tyle razu, ile aktualizowany był stan komponentów. Od Reacta 18 wszystkie rendery bez wyjątków będą grupowane.
// Before: only React events were batched.
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React will render twice, once for each state update (no batching)
}, 1000);
// After: updates inside of timeouts, promises,
// native event handlers or any other event are batched.
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
}, 1000);
Nowe zastosowania Suspense i Suspense na serwerze
Suspense do tej pory pełnił w React tylko jedną funkcję: w połączeniu z React.lazy umożliwiał wyświetlanie zapasowego komponentu do czasu, aż kod docelowego komponentu zostanie pobrany. Od teraz Suspense w pełni wykorzystywał będzie tryb współbieżny, co oznacza, że będzi on wyświetlał zapasowy komponent za każdym razem, kiedy jego dzieci będą renderowane. W połączeniu z startTransition, ma to umożliwić zaawansowaną obsługę asynchronicznych akcji, takich jak na przykład pobieranie danych z serwera.
const [isPending, startTransition] = useTransition();
function handleClick() {
startTransition(() => {
setTab('comments');
});
}
<Suspense fallback={<Spinner />}>
<div style={{ opacity: isPending ? 0.8 : 1 }}>
{tab === 'photos' ? <Photos /> : <Comments />}
</div>
</Suspense>
Nowy React wnosi do Suspens również możliwość wykorzystania po stronie serwera. Dzięki nowym funkcjom renderToPipeableStream i renderToReadableStream, opóźniona zawartość Suspens będzie streamowana do klienta i będzie podlegała osobnemu procesowi hydration (doładowania skryptów do czystego HTML’a).
Nowy hook useId()
Jeśli kiedykolwiek byliście zmuszeni pisać aplikację, która wspierała renderowanie po stronie serwera, to prawdopodobnie zetknęliście się z problemem generowania losowych zmiennych. W skrócie, standardowa metoda Math.random(), zwróci dwie różne wartości po stronie serwera i po stronie klienta. To natomiast oznacza kłopoty, jeśli wygenerowany w ten sposób id chcieliśmy przypisać do elementu w DOM.
Nowy React dodaje hook useId(), który generował będzie losowe id, spójne po stronie serwera i klienta. Do tej pory podobne rozwiązania oferowały zewnętrzne biblioteki, ale ze względu na wspomniane w poprzednim podrozdziale nowe API do strumieniowania, funkcja taka musiała pojawić się również w bibliotece standardowej.
Zainstaluj teraz i czytaj tylko dobre teksty!
Server Components wciąż są nie stabilne
Od pierwszego przedstawienia React Server Components przez Dana Abramova minęło już sporo czasu. Do dziś pamiętam jak tuż przed wigilią 2020 Dan postanowił wcielić się w Mikołaja i po raz pierwszy zaprezentował publicznie tą innowacyjną koncepcję. React Server Component pozawlają renderować część komponentów po stronie klienta i część po stronie serwera. Takie podejście ma minimalizować ilość kodu wysyłanego do klienta (biblioteki importowane przez komponenty renderowane na serwerze nie są wysyłane do klienta) i dawać dostęp do typowo serwerowych interfejsów z poziomu Reacta (bazy danych, system plików). Oczywiście koncepcja ma też swoje ograniczenia: komponenty renderowane na serwerze nie mogą przechowywać stanu i obsługiwać interaktywnych akcji.
Niestety React 18 to jeszcze nie moment na uznanie React Server Components za stabilne. Co ciekawe w notatce dotyczącej releasu znajdziemy wzmiankę, że na ich stabilizację nie będziemy musieli czekać do wydania React 19 i najprawdopodobniej nastąpi to w jednym z wydań minor.
Źródła:
https://reactjs.org/blog/2022/03/29/react-v18.html
https://reactjs.org/docs/hooks-reference.html#usetransition
https://github.com/reactjs/rfcs/blob/main/text/0213-suspense-in-react-18.md