- React.js: Wprowadzenie do kursu od podstaw
- Poznaj React.js
- Pierwszy komponent w React.js
- Props czyli atrybuty w React.js
- Podział na komponenty w React.js
- Klasy jako komponenty React.js
- Interakcja z komponentami React.js
- Stan komponentów React.js
- State w React.js 2
- Metody cyklu życia komponentu w React.js
- React.js w przykładach: filtrowanie statycznej listy
- Tworzenie aplikacji React.js dzięki create-react-app
- React.js na GitHub Pages dzięki create-react-app
- Testowanie aplikacji React.js — podstawy Enzyme
- Testowanie React.js w Enzyme — props, state i interakcje
- Poprawne bindowanie funkcji w React.js
- Odpowiadam na pytania: Babel, ECMAScript, destrukturyzacja, onClick, className
- Komunikacja pomiędzy komponentami w React.js
- Komunikacja z API w React.js
- Formularze w React.js — kontrolowane komponenty
- Formularze w React.js — niekontrolowane komponenty
- Odpowiadam na pytania: props, nawiasy klamrowe, funkcje vs klasy, import react
- TDD w React.js z pomocą react-testing-library
- Flux i Redux: globalny store i jednokierunkowy przepływ danych
- React + Redux — kurs: wprowadzenie i podstawy
- React + Redux — filtrowanie listy, proste selektory
- Projektowanie komponentów: Presentational & Container Components
- Asynchroniczność w Redux: redux-thunk
- Kiedy używać state, a kiedy Redux?
- Nowe metody cyklu życia: getDerivedStateFromProps i getSnapshotBeforeUpdate
- Leniwe ładowanie komponentów w React dzięki import
- Higher Order Reducers — Redux i powtarzanie kodu
- React Hooks — wprowadzenie i motywacja
- React Hooks: useState, czyli stan w komponentach funkcyjnych
- React Hooks: useState — wiele stanów, callbacki i inne niuanse
- React Hooks: useEffect — efekty uboczne w komponencie
- React Hooks a żądania do API
- useReducer — przenoszenie logiki poza komponent
- useMemo, useCallback, czyli rozwiązanie problemów ze zmieniającymi się propsami
- Wady React Hooks
- React Hooks: Piszemy własne hooki!
Od wielu tygodni nie opada kurz po zamieszaniu związanym z React Hooks. Poziom hype przebił wszelki hajpometry, a rozsądna debata na temat kodu została zastąpiona prześciganiem się w pisaniu co raz to sprytniejszych i czystszych (w sensie pure) reReact Hooków. Zastanówmy się jednak nad wadami tego rozwiązania.
Dlaczego tak działamy i po raz n-ty dajemy się ponieść emocjom, mimo tego, że przecież na podstawie wielu doświadczeń mamy świadomość, że to zgubne? Tego nie wiem i na to pytanie nie będę w stanie udzielić odpowiedzi. Uznałem jednak, że radosnemu uniesieniu na temat Hooków przyda się zdroworozsądkowe podejście i szczypta krytyki. A może bardziej pięść w brzuch.
Po co nam React Hooks?
Po co nam hooki? Żeby ułatwić wprowadzenie Suspense, żeby ludzie nie robili głupich rzeczy, żeby nie było mutacji, żeby kod się lepiej minifikował, żeby można było wydzielać logikę zawierającą stan poza komponenty. Zalety te opisywałem w artykule React Hooks — wprowadzenie i motywacja. Jeśli spojrzysz na to z dystansu, to dostrzeżesz, że React, niczym socjalizm, świetnie rozwiązuje problemy, które sam stworzył.
Reaktywność i mutacje
Głównym argumentem za unikaniem mutacji w React jest utrata spójności pomiędzy interfejsem, a danymi. To brzmi świetnie i bez kontekstu tylko bym przytakiwał. Mutacje są be i nie lubię mutacji. Tyle, że inne frameworki pokazują, że wcale nie musi tak być. Knockout, Angular, Vue.js, Svelte dowodzą, że idea reaktywności i sprytnych mutacji działa lepiej, jest bardziej przystępna i zrozumiała dla ludzi, niż całkowita czystość, do której dąży React. „Czystość”, huh? Wrócimy do tego.
Bardzo znaną biblioteką do zarządzania stanem w React jest MobX. Dlaczego stał się tak popularny? Bo pozwala na mutacje i nie straszy negatywnymi konsekwencjami. Ludzie lubią mutacje. Ludzie chcą mutować dane. Dajcie ludziom mutacje w kontrolowanym środowisku, jak MobX, jak Vue.js, a nie zrobią sobie krzywdy i będą mega produktywni. Tak, pisze to człowiek, który absolutnie uwielbia paradygmaty programowania funkcyjnego. Ale nader wszystko lubię pragmatyzm.
Czystość
Czystość funkcji jest często wskazywana jako ogromna zaleta Reacta. Tyle, że komponenty funkcyjne korzystające z Hooków wcale nie są pure functions. To kłamstwo, które zostało bezmyślnie powtórzone przez tysiące osób, ale mimo to nie stało się prawdą.
Czysta funkcja to taka, która dla tych samych danych zawsze zwraca ten sam rezultat, a jej wynik zależy wyłącznie od przekazanych do niej argumentów. Pure function jest na przykład const fn = x => x + 2;
, a nie jest const fn = x => x + Math.random();
. Komponenty funkcyjne używające React Hooks nie są więc pure, bo ich wynik zależy właśnie od Hooków!
Autorzy później poprawili się, że chodziło o czystość tylko w pewnym sensie. Jakim? Czy to ułatwia, czy utrudnia zrozumienie Hooków osobom zaprzyjaźnionym z programowaniem funkcyjnym? A wszystkim pozostałym? Znowu: nie jestem w stanie odpowiedzieć na te pytania, ale sądzę, że powstałe zamieszanie w nazewnictwie nie pomaga nikomu.
Krytyka React Hooks
Skoro już jesteśmy przy nazwach, i tak, wiem, to w sumie nieistotne, nazwy można zmienić, bla bla bla… Srsly, useEffect
? Chodzi o efekt algebraiczny? Efekt uboczny? O co chodzi? Ani o jedno, ani o drugie, a o funkcję wywoływaną przy każdym renderze! Szybko powstała paczka, która tę niejasną nazwę zmienia i rozbija na 3 hooki: useDidMount
, useDidUpdate
i useWillUnmount
, bo tak naprawdę tym właśnie jest useEffect
, a nie żadnym „efektem”.
Żeby było bardziej myląco, to efekty algebraiczne w React też są. Na tyle, na ile pozwala na to sam język, ale są. Rzucanie wyjątku z Promisem w środku w renderze, jak głupio by nie brzmiało, to właśnie Reactowa próba implementacji efektów algebraicznych.
Subtelne bugi
Autorzy Hooków mówią, że Hooki pozwalają uwolnić się od subtelnych bugów w aplikacjach, takich, jak np. zapominanie o pozbyciu się subskrypcji, gdy komponent jest usuwany z drzewa. To półprawda. Czyli, że kłamstwo. Ten problem istnieje, ale Hooki go nie rozwiązują. Robiłem code review wielu fragmentów kodu, w których autorzy zapominali usunąć subskrypcje w useEffect
, pomimo Hooków, pomimo dokumentacji, która co rusz o tym wspomina.
class Component1 extends React.Component {
componentDidMount() {
this.props.subscribe();
}
}
function Component2({subscribe}) {
React.useEffect(() => {
subscribe();
});
}
W powyższym przykładzie oba komponenty mają ten sam bug. Co gorsza, komponent funkcyjny ma również drugi błąd. Potrafisz dostrzec, jaki?
Czy lepszą dokumentacją, ewangelizacją albo zasadami ESLint można naprawić ten problem? Pewnie tak. Ale wtedy to nie ma absolutnie żadnego związku z Hookami.
Refy
Każdy, kto próbuje używać Hooków w końcu natknie się na ten sam problem: Bardzo prosty kod po przeniesieniu z klasy do funkcji działa niepoprawnie, ale w niezwykle subtelny sposób. Dobrym przykładem jest jakiekolwiek wywołanie asynchroniczne i odnoszenie się do props
: w komponentach klasowych zawartość props
będzie zawsze aktualna, nawet asynchronicznie po jakimś czasie. Natomiast w funkcyjnych będą to propsy z momentu wywołania asynchronicznej operacji. Jeśli pomiędzy wywołaniem, a zakończeniem działania propsy się zmienią, to w przypadku komponentu funkcyjnego nasza asynchroniczna funkcja tego nie zobaczy.
Jest to zrozumiałe i wynika z prostej różnicy pomiędzy komponentami funkcyjnymi, a klasowymi: W funkcyjnym render polega na wywołaniu ponownie funkcji, a więc przekazaniu zupełnie nowego obiektu props jako argument. Asynchroniczny callback trzyma jednak referencję do poprzedniego obiektu props, a nie tego nowego. Komponenty klasowe tego problemu nie mają, bo, uwaga, jest tam ukryta mutacja! Tak. this
jest mutowalne i React je mutuje podmieniając stare this.props
na nowe this.props
. Tym sposobem asynchroniczny callback zawsze może się odwołać do aktualnych propsów.
class Component1 extends React.Component {
componentDidMount() {
this.props.subscribe(() => {
console.log(this.props.value);
});
}
}
function Component2({subscribe, value}) {
React.useEffect(() => {
subscribe(() => {
console.log(value);
});
});
}
Powyższy komponent funkcyjny, oprócz znanych nam już dwóch bugów, ma też nowy błąd: Odwołuje się do nieaktualnych propsów (stale props). Żeby popełnić taką pomyłkę w komponencie klasowym, trzeba się naprawdę natrudzić.
Teraz, jak rozwiązać ten problem w Hookach? Użyć mutowalnych refów. Ta mutacja, która do tej pory była ukryta, kontrolowana i działa się samoistnie (robił ją za nas React) teraz będzie musiała być robiona ręcznie, w sposób niekontrolowany, ze wszystkimi idącymi za tym zagrożeniami i wadami. Wow, to jest pewnie ta reklamowana czystość.
Eslint
ESLint miał naprawić wszystko. Rzeczywiście, czasem jego podpowiedzi do Hooków są naprawdę sprytne. Ale częściej nie. Wziąłem kod powyżej i doprowadziłem do stanu używalności (naprawiłem jeden bug). Hook wygląda teraz tak:
React.useEffect(() => {
subscribe(() => {
setValues(vals => [...vals, value]);
});
}, []);
Efekt to aplikacja, która pozornie działa, ale nadal ma najważniejszy bug: odczytuje nieaktualne już propsy. Oczekiwanym efektem jest to, że oba komponenty renderują tę samą listę.
Ale, ale, chwila, moment, eslint daje mi jakieś ostrzeżenie!
React Hook React.useEffect has missing dependencies: 'subscribe' and 'value'. Either include them or remove the dependency array. If 'subscribe' changes too often, find the parent component that defines it and wrap that definition in useCallback.
No dobra, dodajmy subscribe
i value
do tablicy. Co się wtedy stanie? Ostrzeżenie zniknęło. Kod wygląda tak, jak widać poniżej. Ale czy działa?
React.useEffect(() => {
subscribe(() => {
setValues(vals => [...vals, value]);
});
}, [subscribe, value]);
Co się dzieje? Jest tylko gorzej! Nie ma ostrzeżeń. Kod źle działa. Eslincie, miałeś byś taki mądry 🤔 Kod dostępny live tutaj: https://codesandbox.io/s/divine-violet-z0uyq
Powyższy fragment jest uproszczoną wersją autentycznego kodu jednej z osób, której robiłem code review. I tak, nie ufała mi, kiedy mówiłem, żeby przestała ślepo słuchać eslinta, bo bug jest gdzieś indziej. Nie uwierzyła, że musi użyć refa. Poszła szukać rozwiązania poza Type of Web. W końcu: przepisała z powrotem na klasę.
useCallback, useMemo
Ktoś zwrócił mi uwagę, że znęcam się tylko na useEffect
, który jest przecież najtrudniejszym z hooków. Racja. Porozmawiajmy o pozostałych.
useCallback
i useMemo
– funkcje potrzebne tylko dlatego, bo React Hooks tworzy więcej problemów, niż rozwiązuje. Spójrzmy na przykład klasy:
class Component3 extends React.PureComponent {
render() {
function doSomething() {
//…
}
return <AnotherComponent onSth={doSomething} />
}
}
Mamy tutaj buga: przy każdym renderze tworzona jest nowa funkcja doSomething
, więc komponent AnotherComponent
będzie się przerenderowywał zawsze, nawet jeśli de facto to będzie dokładnie taka sama funkcja. Rozwiązania są dwa, ale jedno banalne: Nie tworzyć funkcji w renderze!
class Component3 extends React.PureComponent {
doSomething = () => {
// … this.props …
}
render() {
return <AnotherComponent onSth={this.doSomething} />
}
}
Dzięki temu zawsze odwołujemy się do tej samej referencji do tej samej funkcji i komponent AnotherComponent
nie musi się bez sensu przerenderowywać. Jak to wygląda z hookami?
function Component4(props) {
function doSomething() {
// … props …
}
return <AnotherComponent onSth={doSomething} />
}
Mamy buga. Jak go rozwiązujemy? Używając hooka useCallback
!
function Component4(props) {
const doSomething = React.useCallback(() => {
// … props …
}, [/* tu podajemy te propsy, których używamy */]);
return <AnotherComponent onSth={doSomething} />
}
To nieco zaciemnia kod. A teraz zdaj sobie sprawę z tego, że w React.useCallback
musisz opakować każdą funkcję, którą przekazujesz do innego komponentu. Każdą jedną. I w każdej z nich pilnować tablicy zależności. A jeśli w funkcji znajdą się operacje asynchroniczne? Jesteś skazana/y na Refy!
Jeszcze jedno, o czym wcześniej nie wspomniałem: Normalnie w aplikacji większość komponentów klasowych dziedziczy po React.PureComponent
. Zamiana React.PureComponent
na React.Component
i odwrotnie to tylko kilka znaków, nie zaciemnia kodu w ogóle. W przypadku Hooków, niestety, musimy cały komponent opakować w React.memo
. A więc tak naprawdę powyższy kod wyglądałby o w ten sposób:
const Component4 = React.memo((props) => {
const doSomething = React.useCallback(() => {
// … props …
}, [/* tu podajemy te propsy, których używamy */]);
return <AnotherComponent onSth={doSomething} />
});
Według mnie to ogromny narzut mentalny i zaciemnienie kodu.
Błędy
Autorzy Hooków mówią, że Hooki pozwalają uwolnić się od subtelnych bugów w aplikacjach, ale nie wspominają, że przynoszą one jeszcze więcej miejsc, w których takie błędy potencjalnie można zrobić. Co gorsza, trudno na pierwszy rzut oka dostrzec, co jest z takim kodem nie tak, a jeszcze trudniej znaleźć rozwiązanie. O ile w przypadku klas każdy jest w stanie wykombinować lepszy lub gorszy sposób, tak w przypadku Hooków jesteśmy skazani na jedną słuszną pseudo-funkcyjną metodę rozwiązywania problemów. Przypomina mi się konwersacja, którą niedawno miałem z koleżanką:
– Zobacz, jaki napisałem Hook, ale super, prawda?
– No, wygląda nieźle. Ale niestety w takim i takim bardzo rzadkim przypadku nie zadziała.
– Uh. Okej. (…) Przepisałem to na klasę, zobacz.
– Znacznie prościej.
Jeśli mi nie wierzysz, to zaimplementuj hook useInterval
, który wywołuje przekazaną funkcję co jakiś czas. Bez googlania. W klasie to przecież banał, nie? W Hookach nie powinno to być dużo trudnie… OŻ W MORDĘ.
Klasa. Trochę sobie utrudniłem dodając reagowanie na zmianę propsa time
powodujące ustawienie nowego interwału:
class Component5 extends React.Component {
timerId = 0;
startInterval = () => {
clearInterval(this.timerId);
this.timerId = setInterval(() => this.props.callback(123), this.props.time);
}
componentDidMount() {
this.startInterval();
}
componentWillUnmount() {
clearInterval(this.timerId);
}
componentDidUpdate(props) {
if (this.props.time !== props.time) {
this.startInterval();
}
}
}
Wydaje mi się, że ten kod jest w stanie przeczytać i zrozumieć każda osoba, która miała do czynienia z jakimkolwiek językiem programowania.
Funkcja:
function useInterval(callback, delay) {
const savedCallback = React.useRef();
// Remember the latest callback.
React.useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
React.useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
function Component6({ callback, time }) {
useInterval(callback, time);
}
useRef? Dwa useEffect? Ale dlaczego? Czy Twoje rozwiązanie przypomina powyższe? Ten kod wziąłem z bloga Dana Abramova. Sam bym pewnie go napisał, ale zajęłoby to tydzień. Nie można jakoś tak… łatwiej? Można! Klasą.
Reużywalność logiki
Teraz na pewno ktoś rozpocznie linię argumentacyjną pod tytułem npm install use-interval
. Hooki można łatwo wydzielać i przenosić. Łatwiej, niż analogiczny kod z klasy, to prawda. Tylko co z tego? Ile kodu jesteś w stanie zainstalować z npm? Czy weryfikujesz jego poprawność? Czy autorki i autorzy paczek na npm nie popełniają błędów? O ile łatwiej jest popełnić taki lub podobny, bardzo subtelny błąd w Hookach? Poza tym, nieco mniej wygodny, ale równie „reużywalne” fragmenty kodu można tworzyć używając HoC lub render propsów.
Po co nam Hooki
Każdy zna taką osobę, która próbuje realizować jakiś projekt poboczny w 100% podążając za jednym paradygmatem albo w jednym ezoterycznym języku, no nie? Trochę się podśmiewamy z braku realistycznego podejścia do życia, ale też odczuwamy podziw do oddania się ideom. Każdy lubi się pochwalić napisaniem super skomplikowanego kodu, który jednak jest czysty i zgodny z jakimiś pryncypiami.
No i dokładnie takie są moje odczucia względem React Hooks. Tak, jakbym patrzył na kod tego rąbniętego znajomego, który wszystko pisze w Haskellu i podtyka innym pod nos mówiąc „pochwal mnie, jaki jestem mądry”. Dokładnie tak się czuję, gdy patrzę na React Hooks.
Wiele osób przychodzi do mnie, żeby pokazać mi Hooki, które zostały przez nie napisane. Hooki, które rozwiązują naprawdę banalne problemy, ale z powodów opisanych wcześniej, sam kod jest skomplikowany i zawoalowany. useInterval
. useDebounce
. Gdyby ten sam problem rozwiązały przy pomocy klasy, nigdy nie przyszłyby się chwalić, bo napisanie klasy nie wzbudzałoby takiego podziwu, gdyż byłoby łatwe, proste, przyjemne i czytelne. Wow, hook, wow, pure, coś tam coś tam algebraic effect coś tam, wow. Chcę zapytać: But why?
Postęp
Niektórzy mówią mi, że mam rację, ale nie da się inaczej. Że tak jest dobrze. Że lepiej, niż było. Że to postęp. Żebym spojrzał z szerszej perspektywy na zalety React Hooks.
Wzdycham wtedy i mówię: Moi drodzy, nie urodziłem się wczoraj. Lata temu z zacięciem broniłem rozwiązań, które wydawały mi się rewolucyjne, bo sądziłem, że nie da się tego samego problemu rozwiązać lepiej. Przykładem niech będzie router Angulara i dynamiczne ładowanie komponentów. Wtedy zdawało mi się, że nie da się lepiej. Dzisiaj wiem, że byłem krótkowzroczny, a prostsze i jednocześnie bardziej elastyczne rozwiązania były w zasięgu ręki. Da się. Zawsze się da.
Czasem potrzeba stworzyć potworka, żeby ktoś ze społeczności powiedział „mam lepszy pomysł”, a frustracja przy pracy z potworkiem popchnęła większą rzeszę ludzi do spróbowania tego lepszego pomysłu. Trzeba tylko zrobić krok wstecz.
Podsumowanie
Czy Hooki rozwiązują te problemy, których rozwiązanie obiecali nam twórcy? Trochę. Wprowadzają też sporo nowych kłopotów. Pytam więc: czy da się lepiej, a przede wszystkim prościej? Wygląda na to, że tak. Spójrzmy na Svelte. Spójrzmy na Vue.js 3. A potem spójrzmy na Reacta. Coś zdecydowanie poszło nie tak.
A jakie jest Twoje zdanie? Napisz koniecznie w komentarzach!
- React.js: Wprowadzenie do kursu od podstaw
- Poznaj React.js
- Pierwszy komponent w React.js
- Props czyli atrybuty w React.js
- Podział na komponenty w React.js
- Klasy jako komponenty React.js
- Interakcja z komponentami React.js
- Stan komponentów React.js
- State w React.js 2
- Metody cyklu życia komponentu w React.js
- React.js w przykładach: filtrowanie statycznej listy
- Tworzenie aplikacji React.js dzięki create-react-app
- React.js na GitHub Pages dzięki create-react-app
- Testowanie aplikacji React.js — podstawy Enzyme
- Testowanie React.js w Enzyme — props, state i interakcje
- Poprawne bindowanie funkcji w React.js
- Odpowiadam na pytania: Babel, ECMAScript, destrukturyzacja, onClick, className
- Komunikacja pomiędzy komponentami w React.js
- Komunikacja z API w React.js
- Formularze w React.js — kontrolowane komponenty
- Formularze w React.js — niekontrolowane komponenty
- Odpowiadam na pytania: props, nawiasy klamrowe, funkcje vs klasy, import react
- TDD w React.js z pomocą react-testing-library
- Flux i Redux: globalny store i jednokierunkowy przepływ danych
- React + Redux — kurs: wprowadzenie i podstawy
- React + Redux — filtrowanie listy, proste selektory
- Projektowanie komponentów: Presentational & Container Components
- Asynchroniczność w Redux: redux-thunk
- Kiedy używać state, a kiedy Redux?
- Nowe metody cyklu życia: getDerivedStateFromProps i getSnapshotBeforeUpdate
- Leniwe ładowanie komponentów w React dzięki import
- Higher Order Reducers — Redux i powtarzanie kodu
- React Hooks — wprowadzenie i motywacja
- React Hooks: useState, czyli stan w komponentach funkcyjnych
- React Hooks: useState — wiele stanów, callbacki i inne niuanse
- React Hooks: useEffect — efekty uboczne w komponencie
- React Hooks a żądania do API
- useReducer — przenoszenie logiki poza komponent
- useMemo, useCallback, czyli rozwiązanie problemów ze zmieniającymi się propsami
- Wady React Hooks
- React Hooks: Piszemy własne hooki!
Hej, dzięki za ciekawy artykuł!
Mała poprawka – do komponentów używamy React.memo, a useMemo do „ciężkich” obliczeń 😉
https://reactjs.org/docs/react-api.html#reactmemo
Ktoś już zwrócił mi na to uwagę i od razu poprawiłem – to zwykła literówka. Jeśli nadal widzisz stary tekst, to pewnie cache serwera. Za jakiś czas powinno być ok!
Bardzo potrzebny głos w dyskusji. Takie teksty zawsze są przydatne w momentach niebezpiecznie rosnącego hype-driven-developmentu; tak dla przypomnienia, że to tylko kolejny rodzaj młotka. 😉
Dzięki 🙂
Świetne podsumowanie – dla kilkuletniego Angularowca i przede wszystkim – front-endowca – hooks to jest niestety nieporozumienie od strony DX. Niestety, pokazuje też najgorszą moim zdaniem cechę środowiska Reactowego (o której powstała książka „Nowe szaty króla”).
Przerzuć to proszę na angielski jak znajdziesz czas, bo chętnie poczytałbym odpowiedzi fanów tego podejścia na ten artykuł.
Kilka osób to już sugerowało, więc tłumaczenie pewnie powstanie przez weekend 🙂 Dzięki za komentarz.
IMHO o wiele lepszy dx daje rekatywnosc. Vue js 3 zrobiło swoje hooki i zrobiło to dobrze, warto spojrzeć.
ESLint nie pomoże gdy autor nie potrafi korzystać z useState.
https://codesandbox.io/s/distracted-wright-fh48t
W artykule nic nie ma o useState. Niestety Twoje linki mi się nie ładują, ale czy chodziło Ci o useEffect? To zupełnie różne hooki. Umiem z niego korzystać 🙂 Nie pisałbym artykułu, gdybym nie umiał!
Sprawdzę Twoje kod ponownie wieczorem, może wtedy zadziała, ale w międzyczasie możesz przejrzeć poprzednie części kursu, w których wyjaśniam, jak to działa. Powinno pomóc Ci zrozumieć ten wpis, bo bez wiedzy o hookach rzeczywiście może być ciężko.
W artykule (i podlinkowanym sandboxie) wykorzystujesz (błędnie) useState.
setValues(vals => [...vals, value]);
Rzeczywiście nie jest to do końca jasne z treści, ale robię to celowo, aby zobrazować problem i coś się renderowało. Zamiast
setValues
wstaw tam dowolną inną funkcję, choćbyconsole.log
, a zauważysz problem, którego nie da się rozwiązać bez refów.Czekaj, dlaczego uważasz to użycie useState za błędne? Jest jak najbardziej poprawne. Natomiast Twój kod zawiera błąd 🙂 Nie można ustawiać w ten sposób stanu na podstawie poprzedniego stanu. Aby to zrobić musisz przekazać jako argument funkcję. Tak samo jest i było zawsze, niezależnie, czy w klasach, czy w funkcjach. W innym wypadku narażasz się na race conditions i ryzyko utraty danych.
Co by było, gdyby stan został zmieniony w innym miejscu w międzyczasie? W swoim kodzie straciłbyś część informacji.
Drugi bug w Twoim kodzie to to, że funkcja
subscribe
wywoływana jest przez komponent wiele razy, przy każdej zmianievalue
, a powinna być tylko jeden raz. Podpinanie się do tej samej subskrypcji w kółko szybko spowoduje znaczny wyciek pamięci, a może nawet zawieszenie przeglądarki. Widać to łatwo, gdy doda sięconsole.log
:Tak naprawdę więc swoimi zmianami tylko zamaskowałeś dużo poważniejsze błędy! To właśnie mam na myśli mówiąc o trudności hooków: Pozornie dobrze działający kod ma subtelny bug, który bardzo trudno wyśledzić i jeszcze trudniej rozwiązać!
useState to nie to samo co this.setState w komponencie klasowym. useState masz osobne do każdej zmiennej. W ilu miejscach w kodzie zmieniasz jedną zmienną na podstawie jej poprzedniej wartości?
Nie ma znaczenia w ilu miejscach, liczy się, żeby robić to poprawnie wtedy, gdy jest taka potrzeba – czyli na przykład w tym kodzie. Dokładnie tak samo jest w this.setState.
A da się ten przykład zrobić poprawnie na hook’ach?
W ogóle to:
Zwiesza mi się po wyświetleniu 15 wartości. Niektóre się powtarzają. Zarówno w klasowym jak i w funkcyjnym. Co oto się dzieje? Jakiś memory leak?
Tak, jest tam bug, który może zawieszać przeglądarkę. Komponent w kółko wywołuje funkcję
subscribe
i przez to w końcu się zwiesi.Czy da się to poprawnie zrobić na Hookach? Tak! Używając mutowalnego refa: https://codesandbox.io/s/dazzling-galois-e7g20
Zrobiłem bez ref’ów i działa:
https://codesandbox.io/s/priceless-surf-pz6h0
Jedynie co zrobiłem to zaimplementowałem unsubscribe. Natomiast fakt, wywoływanie w kółko unsubscribe/subscribe jest tu niepotrzebnym narzutem.
Czyli właściwie z punktu widzenia poprawności (nie wydajności) we wszystkich tych próbach naprawy błędem było wielokrotne dopisanie się do subscribe bez usunięcia po sobie starej?
Można tak. Ale chodziło mi o stworzenie przykładu, w którym naprawa problemu jest a) niebanalna i b) eslint źle podpowiada rozwiązanie 🙂
Subscribe tutaj był najprostszym, co przyszło mi do głowy, ale można wymyślić też inny przyklad, w którym będą dokładnie te same problemy.
Oczywiście, wszystkie da się rozwiązać – ale nie jest to oczywiste.
eslint z exhaustive-deps i prettier w WebStorm jeszcze lepsze jaja potrafi zrobić. Mianowicie sam wypełnić deps wszystkim, co jest używane w useEffect. Kiedyś zrobiłem sobie forka tego: https://github.com/joepuzzo/informed (początek lipca 2019). Puściłem elegancko prettier i przeglądarka się zawiesiła 😉 okazało się, że przyczyną było wywołanie się useEffect, która miała za dużo w deps i wywoływała się w efekcie przy każdym renderze. A ona znowu coś tam zmieniała i to wymuszało kolejny render i tak szedł w kółko. https://github.com/joepuzzo/informed/blob/40efaccf1cf4b00019a589735e45b25186d76fb0/src/hooks/useField.js – gość nawet okomentował to „// This is VERYYYY!! Important!”, że deps mają być takie, a nie inne.
Pytanie moje, czy w takim razie taki useEffect, który używa tego co jest zmienne i nie ma tego w deps, jest w ogóle poprawny? Nie powinno się własnie mutowalnego refa użyć?
Ta biblioteka w wersji 1.x robiła to wszystko na HoC. Potem w 2.x przepisał wszystko na hook’i.
Może być poprawny, jeśli wiesz co robisz 🙂 Ale, jak widać, nie jest to ani intuicyjne, ani czytelne…
Komponent, który ma służyć jako przykład jak łatwo jest pisać komponenty klasowe nie działa.
https://codesandbox.io/s/ecstatic-euclid-4lve6
Brakuje w nim funkcji
componentDidMount() { this.startInterval() }
Brakuje też
componentWillUnmount() { clearInterval(this.timerId) }
jeżeli ma działać tak jak hook useInterval.Poza tym ma buga: przy pierwszym renderze usuwa interval o id 0 (jeśli gdzie indziej w aplikacji tworzone są intervale to niespodziewanie jeden z nich zniknie).
Słuszna uwaga, potem poprawię.
Komponent już poprawiony.
Jeśli chodzi o timer, to nie ma tam żadnego błędu. TimerID nigdy nie może być równe zero (timery to tylko liczby dodatnie), więc ustawienie go początkowo na 0 jest jak najbardziej poprawne i bezpieczne. Nie ma ryzyka konfliktu.
To zły pomysł. W większości przypadków dodawanie useCallback tylko spowolni kod.
https://kentcdodds.com/blog/usememo-and-usecallback
Nie wiem, skąd bierzesz takie informacje, ale to nieprawda i linkowany post nic takiego nie mówi 🙂 Odpisywałem dłużej już komuś na Facebooku, napiszę kolejny artykuł, żeby wyjaśnić tę często powtarzaną nieprawdę.
Jak najbardziej to prawda, w najnowszych wersjach reacta re-render nie jest problemem bo jest szybki i nie trzeba wszystkiego opakowywać w useCallback czy useMemo, sami autorzy o tym mówią a znaleźć też można wiele artykułów na ten temat, które sprawdzają wydajność i mówią wprost że rzadko kiedy useCallback trzeba użyć 🙂
Mógłbyś podlinkować, gdzie autorzy o tym mówią oraz do jakichś benchmarków?
Np tutaj https://github.com/facebook/react/issues/16437, rerender nie jest zly jest jest „cheap” a przewaznie jest w innych przypadkach dopiero MUSISZ uzyc useCallback, raczej optymalizujemy gdy widziny problem a nie na wszelki wypadek
Czyli nie masz linka do żadnych benchmarków?
Problem polega na tym, że to, o czym mówisz ma zastosowanie wyłącznie w komponentach funkcyjnych. W klasowych ten problem w ogóle nie występował, nigdy nie trzeba było się zastanawiać, czy keszować funkcje, czy nie, bo to zawsze po prostu działo się samo 🙂
PS Wrzuciłem wcześniej link do poprawionego setState. Tak wygląda pełna implementacja subskrypcji za pomocą hooków.
https://codesandbox.io/s/elated-browser-7r7k7
Okej, widzę już kod – to dopiero pokazuję skalę problemu i skomplikowania 😵
A linkowany przez Ciebie wcześniej „poprawiony setState” wprowadza dwa bardzo poważne bugi i tylko maskuje problem. Tak naprawdę aplikacja „działa” tylko przypadkiem. Dokładnie odpisałem w komentarzu niżej.
Widać tutaj, jak na dłoni, że rozumienie nawet podstawowych hooków sprawia trudność praktycznie wszystkim osobom, nawet tym, które chcą uchodzić za ekspertów. I między innymi właśnie o tym problemie mówi mój artykuł 🙂
Niestety żaden z Twoich linków mi się nie ładuje (to pewnie wina mojego internetu?), więc nie sprawdzę, czy dobrze to napisałeś. Spróbuję ponownie wieczorem i dam znać. Dzięki za kod 🙂
A ja mam wrażenie że ten artykuł jest wynikiem jakiejś niewiedzy, obrazy na React 🙂 Czekam na wersję angielską żeby obiegła świat 🙂
Argumenty np takie odnośnie reużywalności i tego że hooki pomagają unikać błędów
są naprawdę niskiego poziomu, czy ten przykład z `useInterval`, skoro nie ma to być reużywlny hook tak jak ten komponent nie jest reużywalny to można to zdecydownie prościej i czytelniej zrobić tak samo pozostałe przykłady. Za mocno subiektywny artykuł, mało merytoryczny. Na szczęście nikt nie nakazuje używania hooków, można korzystać ze starych dobrych „klas” – co jak wiemy w JS też nie jest raczej zalecane 🙂
Jakiej konkretnie niewiedzy?
Dlaczego ten argument jest Twoim zdaniem niskiego poziomu? Nie podając kontrargumentów sprawiasz, że Twój komentarz jest niemerytoryczny i staje się tylko zwykłym ad personam.
Dlaczego
useInterval
ma nie być „reużywalny”? Założeniem hooków jest, aby były one reużywalne – opisywałem to tutaj: https://typeofweb.com/react-hooks-wprowadzenie-i-motywacja/Jak można to zrobić prościej i czytelniej, czy mógłbyś podać przykład? Nie sądzę, aby to miało jakikolwiek związek z „reużywalnością” tego hooka.
Skąd wynalazłeś informację, że używanie klas w JS nie jest zalecane? To po prostu zwykła bzdura. Będę wdzięczny za wskazanie źródła, które rozsiewa takie informacje i wprowadza początkujących w błąd.
Tak jest zalozenie ze maja byc reuzywalne ale to porownaj kod takiej reuzywalnej klasy i wtedy bedzie to mialo sens 🙂 Tezy postawione w artykule maja w sobie duzo prawdy ale sa mocno subiektywne, ale tez pewnie taki mial ten artykul byc
No nie ma problemu, możemy porównać 🙂 Możemy zrobić HoC albo render propsa – będzie trochę więcej kodu, ale nie będzie on bardziej skomplikowany.
Co do klas to to po prostu nie sa prawdziwe klasy jak w innych jezykach 🙂 https://www.toptal.com/javascript/es6-class-chaos-keeps-js-developer-up
Kolejny argument to Abramov sam pisal ze ludzie myslą ze w tych komponentach klasowych nie ma magii a jest wiecej niz w hookach
Klasy, jak klasy. To, że działają inaczej, niż w niektórych językach nie oznacza, że nie są „prawdziwe”.
W komponentach klasowych nie ma „magii”. Tak samo, jak nie ma jej w Hookach. Jest po prostu kod. Mówienie o czymś „magia” według mnie świadczy raczej o lenistwie i nierozumieniu tematu – no chyba, że to miał być żarcik 😉
dobre podsumowanie, mam podobne zdanie 🙂
Podoba mi się analogia z socjalizmem 😉
…bohaterskie pokonywanie problemów nieistniejących w innych systemach!
😀
Jak napiszesz artykuł w wersji angielskiej to prosiłbym o wrzucenie tego na stronę albo gdzieś na FB. Nie jestem tak biegły w tych tematach by się wypowiadać ale zaskoczył mnie twój wpis i z chęcią poczytałbym jak odniosą się do tego osoby kompetentne.
Siema, jestem javovcem, kursik ogarnalem z ciekawosci i cos tam w nastepnym projekcie ma byc z reactem. Lektura tego kursu i ewolucja kodu react’owego w kolejnych przykladach to jak podróz do jadra ciemnosci.
Mutowalność propsów w komponencie klasowym również prowadzi do bugów i w tym przypadku hooki mają bardziej naturalne zachowanie np taka metoda:
async fetchItem() {
const data = await fetch(`…/${this.props.itemId}`);
// update props.itemId
this.setState({
item: {
itemId: this.props.itemId, // nowy itemId, niezgodny z pobranymi danymi
data,
}
});
}
Czyli jednak wracamy do tego, co napisałem w maju 2019 😀 http://disq.us/p/21xzfl3
No i się z nim zgodziłem 🙂 Wszystko ma wady i zalety.
Zrobiłem takie oto swoje zestawienie wad i zalet na podstawie własnych doświadczeń:
Hooks
Zalety:
– ogromna modułowość i łatwość enkapsulacji. Stan, metody cyklu życia a nawet komponenty/elementy np. z podłączonym ref – pozwala to na łatwe tworzenie czystego deklaratywnego kodu, poprzez schowanie całej imperatywnej logiki
– łatwa kompozycja, jeden hook może bez problemu korzystać z wielu innych
– bardzo dobre wsparcie typowania (nie dotyczy tylko TypeScript, użytkownicy JS również na tym zyskują)
– useEffect w łatwy sposób pozwala na uniknięcie race conditions, ponieważ callback „czyszczący” jest uruchamiany przed każdym następnym uruchomieniem useEffect (w klasie jest to rozsiane po kilku metodach np. cyklu cDU)
Wady:
– wyższy próg wejścia niż klasy (wymaga zrozumienia nowego podejścia do implementacji metod cyklu życia)
– stale closures
Klasa
Zalety:
– niższy próg wejścia (metody cyklu życia są bardziej „explicit”)
– metody nie zmieniają swojej referencji pomiędzy re-renderami
Wady:
– niejawne pochodzenie wartości HOC (nie wiadomo skąd pochodzi dany prop). Co gorsza jeden HOC może nadpisać wartości innego HOCa
– jeden kontekst na klasę
– logika niepowiązana ze soba jest rozsiana po całej klasie nie ze względu na funkcje jakie wykonuje, lecz na używane przez nią metody cyklu życia
– mutowalnie zmienane propsy, które prowadzą do bugów i nieoczekiwanych rezultatów jak zmiana wartości w środku wykonywania asynchronicznej operacji
Super podsumowanie!