Około dwa lata temu, zespół rozwijający React’a po raz pierwszy zaprezentował funkcjonalność biblioteki, którą nazwano React Server Components (RSC). Dzisiaj, dzięki pracy nad rozwojem RSC oraz integracją z meta-framework’ami, możemy sięgnąć po działającą funkcjonalność bez potrzeby własnej konfiguracji, dzięki nowej wersji React’a i Next.js.
Jak uruchomić RSC w Next.js 13.
W Next.js, w wersji 13, możemy już używać nowego podejścia do pisania komponentów. Wystarczy stworzyć katalog app, w którym będziemy umieszczać nasze strony i komponenty oraz dodawać konfigurację appDir w next.config.js:
Katalog app od teraz będzie służył jako miejsce do umieszczania stron (page), i będzie traktował wszystkie komponenty w środku jako „komponenty serwerowe”. Wyjątkiem jest sytuacja, kiedy na początku pliku dodamy dyrektywę 'use client’, wtedy komponent zostanie uznany za „komponent kliencki”.
Jeszcze nie produkcja, ale blisko.
Zespół Next.js jasno zaznacza, że na ten moment katalog app jest w wersji beta, więc moim zdaniem jest za wcześnie, aby myśleć o tym rozwiązaniu na produkcji, jeśli wasza aplikacja jest dojrzałym produktem, na którym opiera się biznes. Nie mniej, beta to już etap dość blisko rozwiązań produkcyjnych, więc dla mniej istotnych aplikacji można pokusić się o małe ryzyko i już testować to rozwiązanie.
Po co? Czy SSR (Server Side Rendering) nie zastępuje tego rozwiązania?
Czasami temat myli się niektórym z SSR, ale należy zaznaczyć, że RSC nie ma na celu zastąpić SSR, tak naprawdę może współpracować razem z nim. Dodatkowo, obydwie rzeczy mogą działać niezależnie, SSR nie jest wymagany do RSC i odwrotnie.
Jeśli trafiliście do tego artykułu, jest wysoce prawdopodobne, że macie już doświadczenie z SSR, nie mniej, w uproszczeniu – przy SSR, serwer w odpowiedzi na żądanie, renderuje aplikację przy użyciu biblioteki react-dom/server. Następnie, zamienia ją na zwykły html (przykładowo używając renderToString), i wysyła w odpowiedzi.
Następnie przeglądarka odbiera html i po załadowaniu plików js z aplikacją „hydratuje” html, co skutkuje „podłączeniem” aplikacji do odebranego drzewa html. Nie będę tutaj wchodził w szczegóły samego SSR, ale jest to dobry punkt odniesienia, aby przedstawić lepiej RSC.
Co powoduje, że w RSC komponenty serwerowe mogą być używane w inny sposób niż zwykłe? Czym to się różni od SSR?
W RSC mamy trochę podobny proces jak w SSR, gdzie serwer też wygeneruje nam html z aplikacji, który wyślemy do przeglądarki, z tą różnicą, że w miejscu komponentów „klienckich” nie będzie tagów html a placeholdery. Następnie aplikacja po stronie przeglądarki, wypełni te placeholdery prawdziwymi komponentami.
Natomiast cały proces nie polega na otrzymaniu html w dokumencie do hydratowania, tylko na wykonywaniu żądań do serwera w celu otrzymania komponentów.
Przykładowo – W przypadku wejścia na stronę aplikacji (w Next.js) będziemy mieli oczywiście SSR, który wyśle przeglądarce html do hydratowania, jednak kiedy będziemy nawigować do następnej strony, React użyje RSC i będzie aktualizować drzewo komponentów komunikując się z serwerem. Warto zwrócić uwagę na to, że aktualizacja drzewa komponentów nie odbywa się w formie zwykłego html, tylko serializacji komponentów do JSON’a.
Taka forma pozwala na prawidłową integrację z serwerem podczas działania w kliencie. Ta kwestia również stwarza nowe wymaganie dotyczące komponentów – Wszystkie propsy, jakie przekazujemy do zwykłych tagów html albo komponentów „klienckich”, z poziomu komponentów serwerowych muszą być możliwe do zserializowania, czyli np. nie mogą być funkcją.
Zalety RSC i sposób użycia
Temat na starcie nie jest najłatwiejszy do zrozumienia, szczególnie bez wiedzy i doświadczenia związanego z SSR, nie mniej, rozwiązanie dostarcza nam dużo możliwości i na pierwszy rzut oka bardzo istotne zalety.
Komponenty serwerowe
Wydzielenie komponentów serwerowych oznacza, jak nazwa wskazuje, że te komponenty zostaną wyrenderowane na serwerze.
Stwórzmy dla przykładu komponent do wyświetlenia daty, statystyk i prostej wizualizacji danych aby poruszyć kilka kwestii.
Jak widać wyżej, importujemy bibliotekę date-fns ułatwiającą pracę z datami, a następnie używamy jej do uzyskania sformatowanej daty dnia dzisiejszego. Nic wielkiego czy trudnego, z tym, że umieściliśmy ten komponent w katalogu app, dzięki czemu automatycznie stał się on komponentem serwerowym.
Zajrzyjmy teraz do bibliotek, które zostały dodane z node_modules w bundlu klienckim:
Tak jak widać, nie znajdziemy tutaj biblioteki date-fns. Co to oznacza? Przeglądarka nie musiała mieć w bundlu tej biblioteki i nie potrzebowała jej użyć do wyrenderowania komponentu. Komponent został wyrenderowany po stronie serwera, tylko po tej stronie potrzebował użyć tej biblioteki. Biblioteka date-fns do najlżejszych nie należy i taka zmiana pozwala wykluczyć ją z bundla klienckiego.
Krótko mówiąc – nasza aplikacja staje się lżejsza i szybsza.
Jest to możliwe, ponieważ biblioteka, której użyliśmy nie ma w sobie komponentów, które potrzebowałyby działać po stronie klienta, wszystkie operacje można wykonać po stronie serwera i z dużą dozą prawdopodobieństwa na serwerze będą wykonane szybciej niż w przeglądarce.
Podobna sytuacja jest nie tylko z bibliotekami, ale samymi komponentami czy po prostu narzędziami, które importujemy.
W bundlu mamy tylko komponenty klienckie i ich zależności. Nie znajdziemy tutaj GraphServerStats czy SomeDataGraph, więc po raz kolejny – jeszcze lżejszy bundle, jeszcze szybszy czas ładowania.
Co z bibliotekami i komponentami „klienckimi”?
Komponenty, które potrzebują działać po stronie klienta, rozumiemy jako komponenty, które np. korzystają z lifecycle React’a, w celu aktualizowania stanu przez useState, czy reagowania na zmiany w komponencie przez useEffect. Przykładów oczywiście może być więcej, ale dość jasno naświetla to różnicę.
Weźmy za przykład komponent StatsDisplay:
Komponent posiada stan pokazujący lub ukrywający statystyki, z tego powodu musimy użyć dyrektywy 'use client’ , która informuje React’a, że jest to komponent kliencki.
Jeśli pominiemy dyrektywę, dostaniemy błąd kompilacji.
Przykładowo, kiedy potrzebujemy użyć biblioteki, która powinna działać po stronie klienta, możemy jej użyć tylko w komponencie, który ma taką dyrektywę.
Co z komponentami klienckimi, które chcą importować komponenty serwerowe?
Aby w komponentach klienckich używać komponentów serwerowych, ale przy tym zachować bardzo ważną funkcjonalność RSC, czyli renderowanie takiego komponentu po stronie serwera i odciążenie przeglądarki, nie możemy użyć zwykłego importu w komponencie klienckim (ponieważ zostanie dodany do bundla), tylko powinniśmy to zrealizować poprzez kompozycję z poziomu komponentu serwerowego.
Przykładowy komponent StatsDisplay, w propsach przyjmuje komponent serwerowy w postaci ReactNode:
Następnie po prostu renderuje go w wyznaczonym miejscu:
„
Czyli w komponencie serwerowym SomeDataGraphs importujemy komponent kliencki StatsDisplay, i dostarczamy mu komponent serwerowy GraphServerStats w propsach:
Dzięki temu GraphServerStats nie znalazł się w bundlu klienckim, ale może być np. dynamicznie pokazywany przez komponent kliencki. Z tego wynika, że, nie ma tak naprawdę ograniczeń co do struktury komponentów, komponenty klienckie mogą renderować komponenty serwerowe i odwrotnie, niezależnie od głębokości zagnieżdżenia.
Komunikacja z back-end’em po nowemu
Dzięki RSC komponenty serwerowe mogą komunikować się z back-end’em bardziej bezpośrednio, zazwyczaj źródła danych czy operacji potrzebne do aplikacji będą „bliżej” serwera, który renderuje RSC niż przeglądarka – która musi przejść przez endpoint użyty za pomocą żądania http.
Przykładowo – w komponentach serwerowych możemy bezpośrednio odwoływać się do serwera, operując na plikach serwera czy nawet pisząc zapytania sql bezpośrednio w komponencie. Nie, żeby pisanie zapytań sql to był „dobry” scenariusz, ale dobrze to pokazuje jakie są możliwości.
W SSR od Next.js możemy pobierać dane czy wykonywać operacje przed wyrenderowaniem strony, poprzez getServerSideProps, jednak kiedy nawigujemy do następnej strony, następne żądania wykona już przeglądarka. Dzięki RSC, komponenty serwerowe, które będą pobrane, aby wyrenderować odświeżone drzewo komponentów, czy np. właśnie następną stronę, wykonają żądania tylko po stronie serwera i przeglądarka nie będzie już musiała tego robić.
Po raz kolejny – aplikacja staje się szybsza.
Meta-frameworki
React Server Components nie jest samodzielnym rozwiązaniem. Jego sposób na serializowanie i deserializowanie komponentów wymaga pomocy bundlera, jak np. webpack, dla którego zespół Reacta tworzy oficjalne rozwiązanie. Poza tym, RSC potrzebuje też wysyłać komponenty z serwera, które po stronie przeglądarki są użyte do wypełnienia drzewa komponentów.
Z pewnością uda się w wielu projektach stworzyć własną, działającą konfigurację, jednak zespół React’a zapowiadał, że wdrażanie funkcjonalności zacznie się przez współpracę z zespołami zajmującymi się rozwojem meta-framework’ów, takich jak np. Next.js. Dlatego, jeśli chcecie użyć RSC w łatwy sposób, polecam sięgnąć po Next.js w wersji minimalnie 13.
Przyszłość
React Server Components nie jest jeszcze powszechnym, 100% stabilnym rozwiązaniem i na pewno da nam więcej zalet niż wymieniłem jednak już teraz widać, że będzie to jedno z najlepszych narzędzi do optymalizacji aplikacji i warto śledzić ten temat.