Podsumowanie czym jest memo w React i co to jest memoizacja komponentu RFC. Konieczne, aby iść dalej. Do dzieła.
Ok, przykład z dokumentacji Reacta, zastanówmy się, co nam mówi ten kod:
import { memo, useState } from 'react';
export default function MyApp() {
const [name, setName] = useState('');
const [address, setAddress] = useState('');
return (
<>
<label>
Name{': '}
<input value={name} onChange={e => setName(e.target.value)} />
</label>
<label>
Address{': '}
<input value={address} onChange={e => setAddress(e.target.value)} />
</label>
<Greeting name={name} />
</>
);
}
Ten kod powinien już teraz mówić nam bardzo wiele:
- Podczas mount rendera ustawione zostaną initial states dla name i address
- Każda zmiana stanu name lub address wywoła diff render RFC MyApp
- RFC MyApp ma child component, któremu przekazuje name jako prop
- Każda zmiana stanu (nawet stanu address) wywołuje diff-render MyApp i w efekcie diff-render komponentu-dziecka, mimo że dziecko tego adresu i powiązanego z nim diff-rendera wcale nie potrzebuje
- Mało ważna teraz rzecz, ale też powinniśmy wychwycić – każdy diffRender przypisuje do onChange nową funkcję, nowy obiekt, o nowej referencji, choć jednakowym działaniu
Ok, teraz zobaczmy na komponent Greeting:
const Greeting = memo(function Greeting({ name }) {
console.log('Greeting was rendered at', new Date().toLocaleTimeString());
const [greeting, setGreeting] = useState('Hello');
return (
<>
<h3>{greeting}{name && ', '}{name}!</h3>
<GreetingSelector value={greeting} onChange={setGreeting} />
</>
);
});
Co widzimy:
- Console.log na poziomie Rfc również będzie wywoływany za każdym renderem (mount render i diff rendery), dobre do debugowania oraz zrozumienia jak działa React i RFC
- Mamy stan greeting
- Stan greeting przekazany jako prop o nazwie value do kolejnego dziecka
- Jako prop onChange przekazana jest funkcja setGreeting, która dziecku pozwala manipulować stanem rodzica
- Tu warto zwrócić uwagę, że funkcje hookowe są inne niż reszta funkcji, ani funkcji setState nie trzeba przekazywać do tablic zależności, ani też nie są nowymi obiektami z nowymi referencjami tworzonymi od zera przy każdym renderze RFC – setGreeting tworzone jest jeden raz na mount render
Ok, czy jest sens robić memo komponentowi GreetingSelector? Cóż, wtedy zmiana stanu rodzica nie będzie triggerować diff-rendera dziecka, o ile nowe propsy nie zostaną przekazane.
Tyle tylko, że jedyną opcją, aby rodzicowi zmienił się stan jest zmiana stanu greeting, który jest przekazywany jako props do GreetingSelector, mało tego, ten stan zmienia się z poziomu GreetingSelector, choć to akurat nie takie ważne.
Zatem nie ma sensu robić memo RFC GreetingSelector, bo nigdy nie będzie takiej zmiany Greeting RFC, która nie triggeruje zmiany propsów GreetingSelector. Sterowanie setGreeting też przekazaliśmy dziecku, to warto też odnotować.
A jak to dziecko wygląda:
function GreetingSelector({ value, onChange }) {
return (
<>
<label>
<input
type="radio"
checked={value === 'Hello'}
onChange={e => onChange('Hello')}
/>
Regular greeting
</label>
<label>
<input
type="radio"
checked={value === 'Hello and welcome'}
onChange={e => onChange('Hello and welcome')}
/>
Enthusiastic greeting
</label>
</>
);
}
Tu mamy do czynienia czarno na białym (już bardziej się nie da) z konceptem Reacta jakim jest jednostronny przepływ danych. To nie działa w dwie strony, że dziecko otrzymuje prop value, modyfikuje prop value i u rodzica zmienia się greeting.
Nie, propsy mają tylko jeden kierunek – z góry na dół. Zatem rodzic ma stan (greeting) i ten stan oraz setState (sterowanie nim) przekazuje dziecku. I dziecko zmieniając stan rodzica wywołuje diff-render rodzica oraz swój własny diff-render z nowymi propsami. I nawet gdyby miało memo – z nowymi propsami, wiemy już do czego służy memo.
I jeszcze jedna uwaga:
- Memo może nas uchronić wówczas, gdy mamy diff-rendery rodzica nie zmieniające propsów dziecka, aby wtedy dziecko się nie diff-renderowało
- Jeżeli mamy memo, ale rodzic w wyniku diff-renderu dokonał zmian propsów dziecka to dziecko też ma diff-render
- Memo nas nie uchroni przed zmianą propsów! A zmiana propsów to może być zmiana referencji obiektu funkcji na poziomie RFC rodzica/dziadka/przodka.
Tak! O tym też pamiętajmy – że jak funkcja jest definiowana bez useCallback na poziomie RFC to każdy diff-render tego RFC to jest definiowanie tej funkcji od nowa, nawet jeśli nie zmienia się nic, żadna logika, zmienia się referencja.
I jeżeli zmienia się referencja a ta funkcja jest przez props przekazywana dziecku, to zmienia się jego props i już niezależnie memo czy nie dziecko ma diff-render.
Memo przed czymś takim nie uchroni, przed tym może useCallback uchronić, który ma właśnie dwa zastosowania:
- Keszowanie funkcji, których definiowanie jest czasochłonne, przez co definiowanie ich co każdy render gdy nic się nie zmienia jest złe dla wydajności
- Keszowanie funkcji, których definiowanie co render wcale nie musi być czasochłonne, ale te funkcje jak stają się nowym obiektem z nową referencją, to niesie to za sobą wszystkie implikacje takie jak:
- Diff render dziecka RFC nawet z memo, bo de facto props się zmienił
- Wywołanie useEffect z zależnością na tą funkcję, bo de facto się zmieniła, choć w deklaratywnym paradygmacie jest to taka sama funkcja, ale tak po prawdzie, ma inną referencję, a komputery mają gdzieś nasze wysokopoziomowe deklaratywne paradygmaty programowania, to też musimy sobie do głowy włożyć
Ok, mam nadzieję, że nie było zbyt trudne. Musimy to zrozumieć, nieważne jak, ale bez tego nie ma sensu zabierać się za projekty używając frameworka, który działa dla nas w sposób niezrozumiały.