Czy warto używać typu FC w React?
React daje nam różne możliwości dodawania specyficznych typów do różnych jego elementów. Możemy to osiągnąć na kilka sposobów. Dziś chciałbym się skupić na typowaniu statycznym i dynamicznym oraz typie FC, który spotkamy w React.
From this article you will learn:
Jakie są różnice między językami statycznie i dynamicznie typowanymi?
Co zmienia w projekcie Javascriptowym wprowadzenie Typescriptu?
Jakie są sposoby typowania w React?
Co zawiera typ ReactNode?
Czy i kiedy warto używać typu FC?
Komponent jest najmniejszą jednostką, z której zbudowana jest aplikacja napisana w React. Warto podkreślić, że komponent w rozumieniu tej biblioteki powinien być reużywalny, w taki sposób, aby kolejne jego wdrożenia były od siebie niezależne.
Innymi słowy komponent to nic innego jak javascriptowa funkcja, do której wywołania przekazujemy argumenty. Pierwszym jej argumentem, który na pewno znasz, jest obiekt, nazwany popularnie props
.
React udostępnia dla nas dwa sposoby na tworzenie komponentów - pierwszym jest komponent funkcyjny, drugim komponent klasowy. Z punktu widzenia Reacta oba komponenty są identyczne. Ponadto żadne rzetelnie wykonane testy wydajnościowe nie wykazały, aby któreś podejście było bardziej optymalne od drugiego. W związku z powyższym, to od nas zależy w jaki sposób chcemy pisać. Warto natomiast dodać, że w ostatnich latach twórcy Reacta dość mocno promują pisanie komponentów w sposób funkcyjny - zacierając różnice pomiędzy oboma podejściami - np. poprzez wprowadzenie hook’ów.
Dziś chciałbym porozmawiać nie tyle o komponentach, ile o ich typowaniu - ale od początku.
Typowanie
Cała magia języka Javascript polega na jego elastyczności, która objawia się na przykład tym, że jest to język dynamicznie typowany. Co rozumiemy pod tym pojęciem? Przede wszystkim - języki dynamicznie typowane pozwalają nam na swobodną zmianę typu danych, które przechowywane są w zmiennej. Co de facto oznacza, że typy nie są sprawdzane podczas uruchomienia programu. Typy danych są w związku z powyższym przypisywane do wartości, które przechowywane są w zmiennych w czasie działania programu. Poniżej przykład działania typowania dynamicznego w PHP:
Jak widać swobodna zmiana typu nie wywołała tutaj żadnego błędu. Podobnie zadziałałoby to w Javascript.
Powyższe podejście stoi w opozycji do typowania statycznego, z którym spotkamy się w językach takich jak Java czy C#. Typ musi być przypisany do zmiennej już podczas jej tworzenia i nie można go zmienić na późniejszych etapach pracy kodu. Próba takiej zmiany zakończy się błędem kompilacji.
Skoro widzimy różnicę pomiędzy językami dynamicznie i statycznie typowanymi powinniśmy zastanowić się, co jest dla nas korzystniejsze z perspektywy pisania kodu. Jako programista pracuję od 2011 roku, przez te lata większość czasu spędziłem w językach dynamicznie typowanych, takich jak JS i PHP. Zawsze doceniałem je za swobodę, którą dają i możliwość stosunkowo szybkiego wytwarzania oprogramowania. Zdecydowanie to są ich zalety. A co z wadami?
Cannot read property X of undefined
W Javascript ten błąd spotkamy często, gdy próbujemy odwołać się do wartości, która jest undefined
. Innymi słowy oznacza on, że zmienna została zadeklarowana, ale nie posiada wartości. Wystąpi on zawsze wtedy, gdy spróbujemy odwołać się do właściwości lub funkcji ze zmiennej, która nie jest obiektem. A może nie być. Wszak to tylko dynamicznie typowany język.
Ten błąd to codzienność frontend developera. Zapytajcie programistę Javy czy w swojej pracy spotkał się kiedykolwiek z podobnym problemem?
Game changer
Świat programistów frontendowych coraz częściej docenia zalety języków statycznie typowanych. Coraz więcej projektów jest pisanych w Typescript, który jest nadzbiorem języka Javascript. Typescript dodaje do JSa statyczne typowanie, ale tylko w procesie dewelopmentu.
Wspaniała wiadomość prawda? Tak właściwie to półprawda, ponieważ ostatecznie przeglądarka i tak działa na kodzie transpilowanym do Javascriptu. Zatem z zalet statycznego typowania możemy korzystać w czasie wytwarzania aplikacji, co i tak stało się niejako game changerem świata front endu.
propTypes
Wróćmy jednak do Reacta. React, z racji tego, że jest napisany w Javascript, nie wymusza w żaden sposób kontroli typów danych, na których pracujemy. Jednak im napisana w nim aplikacja jest większa, tym bardziej sprawdzanie typów może nam się przydać. Tak też powstała odrębna biblioteka prop-types
(wcześniej była ona częścią samego Reacta), która daje nam możliwość dodawania typowania do props’ów naszych komponentów.
Jak to działa?
W powyższym przykładzie ustaliliśmy, że nasze props’y będą zawierały dwa klucze: name
(który będzie stringiem) oraz data
(który będzie obiektem, z kolejnymi dwoma właściwościami age
(number) oraz favorite
(jedna z trzech rzeczy)). To już coś. Natomiast ze względu na wydajność propTypes
sprawdzane są tylko w trybie deweloperskim. Jeśli podamy wartość innego typu niż oczekiwana spowoduje to wyświetlenie w konsoli ostrzeżenia, a na produkcji błędu.
Typescript
Mamy też możliwość użycia w projektach Reactowych Typescipta. Pozwala on nam nie tylko na otypowanie props’ów, ale również całych komponentów i innych elementów naszego kodu. Sam React dostarcza nam całkiem sporo różnych typów, które możemy wykorzystać w swoim projekcie.
Jak widać możliwości mamy dużo więcej niż z prop-types
. Otypowaliśmy nie tylko propsy, ale również cały komponent i argumenty funkcji wewnętrznych. Wygląda na to, że dzięki TS możemy o wiele więcej. Chciałbym się natomiast skupić na jednym typie, którego użyłem powyżej: FunctionComponent
czy też w skrócie FC
.
Po co typować komponenty?
Okazuje się, że komponent funkcyjny możemy otypować na różne sposoby. Tylko po co go typować? Przede wszystkim otypowanie komponentu pozwala nam opisać propsy i określić typ zwracanej wartości. Zwiększy to czytelność naszego kodu i zdecydowanie ułatwi kontrolę tego, co piszemy. React dostarcza nam typ, który możemy wykorzystać do opisania swojego komponentu, tj. wspomniany przed chwilą FunctionComponent
. Komponent możemy zatem otypować wykorzystując FC
lub nie.
Skoro możemy to zrobić za pomocą samego TSa, to po co w ogóle istnieje typ FC? Problemu trzeba szukać w dzieciach komponentu.
Problemy z dziećmi
Komponent Reactowy może przyjąć jeden bardzo specyficzny props, czyli children
. Children to nic innego jak elementy, które zostały przekazane do komponentu w postaci zagnieżdżonych elementów (dzieci w strukturze). W komponencie możemy ten elementy wyrenderować w dowolnie wskazanym przez nas miejscu, wykorzystując właśnie właściwość children
. FC jest typem generycznym, który właściwość children ma odpowiednio opisaną. Gdybyśmy potrzebowali children i typowali samodzielnie komponent, musielibyśmy pamiętać o jego deklaracji w typie. Wyglądało by to następująco:
Wydaje się zatem, że użycie typu FC jest zasadne i rozsądne. Wspiera nas przy typowaniu naszych propsów, daje nam poprawną definicję props’a children i ogólnie skraca zapis.
React.ReactNode
React.ReactNode jest najszerszym typem, którym możemy opisać to, co ma zwrócić komponent funkcyjny. Zwróć uwagę, że może zwrócić o wiele więcej wartości niż tylko element Reacta lub tekst.
Do wersji 18 Reacta zwrócenie wartości undefined z komponentu było praktycznie nie możliwe. React zwracał błąd:
Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
Sprowadzało się to do sytuacji, w której musieliśmy użyć null, aby nic nie zwracać.
W czym więc tkwi problem?
Wracając do dzieci, FC zawsze dodaje informację o children do definicji typu props’ów, nawet gdy nie było ono potrzebne lub nie chcieliśmy tej definicji tam mieć. Dodając informację o children samodzielnie, TS podpowie nam czy nasz komponent powinien mieć dzieci czy nie.
Jak widać na powyższym przykładzie komunikat TSa w tym przypadku jest dość jasny: komponent ComponentA
nie powinien mieć dzieci. Jeśli użyjemy typu FC błędu typu nie będzie.
Inną dość istotną kwestią jest wykorzystanie właściwości defaultProps
do opisywania wartości domyślnych dla propsów. defaultProps
są zaszłością z czasów komponentów klasowych, aczkolwiek nadal można spotkać się z sytuacjami, gdy ich wykorzystanie będzie niezbędne. Niestety łącząc React.FC
i defaultProps
dostaniemy błąd, z którego wynikać będzie, że nie podaliśmy wymaganych propsów do naszego komponentu. Typescript wspiera natomiast defaultProps
już od wersji 3.1.
Updated on 19 July 2023
Powyższy problem został częściowo rozwiązany. Od wersji 18 React'a z domyślnej deklaracji typu FC usunięto children
. Jednocześnie dodano nowy typ generyczny PropsWithChildren
, który zawiera właściwość children jako opcjonalną. Poniżej możesz zobaczyć deklarację nowego typu.
Częściowo jest to zgodne z naszymi oczekiwaniami. Niestety poprzez to, że children są oznaczone jako opcjonalne Typescript nadal nam nie podpowie, że zapomnieliśmy o przekazaniu dzieci, gdy rzeczywiście o tym zapomnimy.
Dodatkowo poprzez zagnieżdżenie kilku typów generycznych kod staje się mniej czytelny i bardziej złożony. Oczywiście jest to tylko moje odczucie, ale wydaje mi się, że osoby które dostaną tak otypowany komponent mogą poczuć się delikatnie pogubione.
Podsumowanie
Używać czy nie używać? Spotkać się można ze stwierdzeniem, że dla początkujących w TS i React o wiele łatwiej będzie wykorzystywać React.FC
, ponieważ daje on względną kontrolę nad tym, co się dzieje w naszych komponentach. Jednak bardziej zaawansowani programiści będą zdecydowanie dążyć do posiadania większej kontroli nad swoimi komponentami.
Na koniec warto dodać, że typ FC zastąpił typ SFC (Stateless Function Component), który w tym momencie jest oznaczony jako przestarzały. W wersji 16.9 został wprowadzony do Reacta VFC (Void Function Component), który miał posłużyć do opisywania komponentów, które nie będą posiadały dzieci. W wersji 18 został jednak uznany za przestarzały. W związku z powyższym zamieszaniem warto się zastanowić czy nie lepiej przejść na samodzielne typowanie funkcji w React. Wokół typu FC bardzo dużo się dzieje. Może nawet za dużo.
Comments (1)
Mateusz
07 maja 2024 o 05:58Dzięki, fajny artykuł :)