W dzisiejszym artykule przedstawię React.memo(), jako sposób na uniknięcie ponownego renderowania i optymalizację komponentów funkcyjnych. React.memo() jest podobne do React.PureComponent w komponentach klasowych.

Zachęcam do postawienia czystego create-react-app i przetestowania React.memo() na żywych przykładach wraz z kodem z artykułu.

„Problem” renderowania

Na początku zaczniemy od tego, że w przypadku, gdy komponent rodzica zostanie przerenderowany, jego dzieci również zostaną przerenderowane… Jest to ważne jeżeli chodzi o optymalizację. Bardzo często zmieniamy stan w komponencie głównym, który nie wpływa na inne komponenty znajdujące się w nim, ale one również zostaną przerenderowane. Poniżej prosty przykład…

import React, {useState} from "react";

const Dziecko = () => (<div>Dziecko: {Math.random()}</div>);
const Dziecko2 = () => (<div>Dziecko2: {Math.random()}</div>);

const Rodzic = () => {
    const [rerender, setRerender] = useState(null);

    return (
        <div>
            Main: {Math.random()}
            <Dziecko/>
            <Dziecko2/>
            <button onClick={() => setRerender(Math.random())}>Render again!</button>
        </div>
    );
};

Powyższy przykład zawiera jeden komponent <Rodzic /> a w nim dwa komponeny <Dziecko> oraz <Dziecko2>. Używamy również hooka useState(), aby po kliknięciu na <button> komponent <Rodzic> został przerenderowany. W tej sytuacji komponeny <Dziecko> oraz <Dziecko2> również się przerenderują. Za każdym razem Math.random() zwróci inną wartość, co będzie dowodem na ponowne renderowanie…

Zmodyfikujemy nasz przykład, aby stworzyć komponent <Header>, który przyjmuje wartość statusu zalogowania.

const Header = ({loginStatus}) => (
    loginStatus ? <div>Hello user {Math.random()}</div> : <div>Please log in {Math.random()}.</div>
);

const Rodzic = () => {
    const [rerender, setRerender] = useState(null);
    const [login, setLogin] = useState(false);

    return (
        <div>
            <b>Main:</b> {Math.random()}
            <br />
            <b>Login status:</b> {login.toString()}
            <br /><br />
            <Header loginStatus={login}/>
            <button onClick={() => setRerender(Math.random())}>Render again!</button>
            <button onClick={() => setLogin(!login)}>log in/log out!</button>
        </div>
    );
};

Jak widać, w naszym przykładzie za każdym przerenderowaniem komponentu <Rodzic>, nawet gdy wartość statusu logowania nie uległa zmianie, komponent <Header> również się przerenderuje. Akurat nasz przykład jest bardzo trywialny i ponowny rerender komponentu <Header> nie jest złożoną operacją. Przyjmijmy jednak, że komponent <Header> jest bardzo skomplikowany, zawiera inne komponenty, które renderują mocno zagnieżdżone menu itd, więc lepiej uniknąć ponownego renderowania bez potrzeby…

W tej sytuacji logiczne byłoby uzależnienie ponownego renderowania komponentu <Header> od statusu logowania. Jeżeli jesteśmy zalogowani i po przerenderowaniu komponentu <Rodzic> status wskazuje, że nadal jesteśmy zalogowani, nie ma sensu ponownie renderować komponentu <Header>. To samo w sytuacji, gdy nie jesteśmy zalogowani i stan ten się nie zmienia. Wykorzystamy do tego React.memo()

React.memo()

Użycie jest bardzo proste i w najprostszej wersji ogranicza się do „wrzucenia” komponentu <Header> w React.memo()

Modyfikujemy komponent <Header>, aby używał React.memo()

const Header = React.memo(
    ({loginStatus}) => (
        loginStatus ? <div>Hello user {Math.random()}</div> : <div>Please log in {Math.random()}.</div>
    )
)

W tej sytuacji React sam zadba o to, aby nie przerenderować komponentu, co będzie widoczne poprzez brak zmiany wartości Math.random()

Uwaga!
Używając React.memo() trzeba pamiętać, że React porównuje wartości poprzez shallow comparision.

Sprawdźmy poniższy przykład. Będziemy przekazywać obiekt, który będzie zawierał tę samą zawartość… Teoretycznie komponent nie powinien być renderowany ponownie, a jednak jest… Właśnie poprzez mechanizm shallow comparision. Za każdym razem otrzymujemy nowy obiekt, mimo że zawiera on te same dane…

const ObjectMemo = React.memo(({inputProps}) => (
    <div>
        {Math.random()}
        {inputProps.name}
    </div>
));

const Rodzic = () => {
    const [objectMemo, setObjectMemo] = useState({name: "test"});

    return (
        <div>
            <b>Main:</b> {Math.random()}
            <br />
            <ObjectMemo inputProps={objectMemo}/>
            <button onClick={() => setObjectMemo({name: "test"})}>Change todolist!</button>
        </div>
    );
};

Tutaj odsyłam też do innego artykułu z bloga, który napisałem wcześniej.
https://www.magicweb.pl/programowanie/frontend/typy-proste-typy-referencyjne/
Artykuł pozwoli zrozumieć dlaczego mimo wykorzystania React.memo() komponent przerenderował się ponownie.

Aby uniknąć takiej sytuacji stworzymy własną funkcję porównującą, gdzie rezultat true lub false determinuje, czy komponent będzie ponownie renderowany, czy nie.

Reaect.memo() i własna funkcja porównująca

React.memo() zapewnia nam możliwość przekazania drugiego parametru, gdzie definiujemy własny warunek, czy komponent ma być przerenderowany czy nie w zależności od tego czy zwróci true czy false. Funkcję można rozpatrywać w kategorii areEqual(), czyli funkcja zwracając true zapobiega przerenderowaniu komponentu, ponieważ wskazujemy że wartości się nie zmieniły.

const areEqualFunction = function (prevProps, nextProps) {
  // dowolna logika oraz zwrócenie true lub false
};

React.memo(Komponent, areEqualFunction);

Przykład z własną funkcją porównującą:

const ObjectMemo = React.memo(({inputProps}) => (
    <div>
        {Math.random()}
        {inputProps.name}
    </div>
), (prevProps, nextProps) => (
    prevProps.name === nextProps.name
));

W tej sytuacji komponent nie przerenderuje się, ponieważ wartość dla właściwości "name" obiektu jest identyczna. Porównanie dwóch stringów z wartością „test” poprzez === zwraca nam true.

Podsumowanie

Kiedy można rozważyć użycie React.memo(), kiedy:

  • komponent przerenderowuje się dosyć często,
  • dla tych samych propsów wejściowych mamy ten sam wynik wyjściowy,
  • komponent rodzica przerenderowuje się, a komponenty wewnątrz rodzica nie zmieniają propsów.

Jak sprawdzić, czy warto użyć React.memo()?
Możemy w tej sytuacji skorzystać z Profilera. Być może w przyszłości napiszę o tym osobny artykuł, ale na razie posłużę się oficjalną stroną Reacta: https://reactjs.org/docs/optimizing-performance.html#profiling-components-with-the-chrome-performance-tab

Źródła:
Obraz: https://unsplash.com/photos/iar-afB0QQw
React.memo() na oficjalnej stronie React: https://pl.reactjs.org/docs/react-api.html#reactmemo