Twórz obiekty i tablice ostrożnie pracując z React Redux i connect!

Spis treści

  1. Wprowadzenie
  2. Implementacja
    1. Liczenie rerenderów komponentu
    2. Podejście standardowe
    3. Podejście z przeniesieniem tworzenia obiektu
  3. Działająca aplikacja na Github Pages
  4. Pełen kod aplikacji na Github
  5. Podsumowanie

Wprowadzenie

W jednym z poprzednich wpisów:

Pisałem o tym, dlaczego należy uważać na przekazywanie obiektów jako propsy – najlepszym wtedy rozwiązaniem wydawało mi się przekazywanie typów prostych które są odporne na problemy z referencjami.

Szczególnie łatwo wpaść w kłopoty używając funkcji connect, czyli sprzed pojawienia się hooków – ale nadal używanej w wielu projektach.

Nie da się jednak ukryć że nie jest to najpraktyczniejsze rozwiązanie i przekazanie obiektu jest często czytelniejsze. Jak więc pogodzić obie zalety – wygodę obiektów i dobrą wydajność?

Kluczowe jest miejsce tworzenia obiektów, już po użyciu funkcji connect – co opiszę w tym wpisie 🙂

Implementacja

Liczenie rerenderów komponentu

Liczenie rerenderów zrealizowane będzie za pomocą hooka useRef, inkrementowane będzie pole current. Komponent będzie miał jako propsy przekazane tytuł oraz obiekt data, który właśnie przez to że będzie obiektem bywa źródłem problemów:

const DataDisplay = ({ title, data }) => {
  const rerenderCount = useRef(0);

  return (
    <div style={{ border: '1px solid black', padding: 16 }}>
      <h2>{title}</h2>
      <h3>{data.name} {data.surname}, elements on invoice:</h3>
      {Object.entries(data.invoice).map(([key, value], index) => (
        <div key={index}>{key}: {value}</div>
       ))}
       <small>Rerenders: {rerenderCount.current++}</small>
      </div>
  );
};

Podejście standardowe

Na nasze potrzeby załóżmy, że dane przekazane do obiektu jako obiekt data składane są z różnych obiektów stanu w Redux:

const mapStateToProps = state => {
  const data = {
    name: state.user.name,
    surname: state.user.surname,
    invoice: state.invoice,
  };

  return {
    title: 'Lots of rerenders',
    data,
  };
};

const Component = props => <DataDisplay {...props} />
return connect(mapStateToProps)(Component);

Pozornie z kodem jest wszystko w porządku, jednak za każdą zmianą stanu komponent DataDisplay będzie rerenderowany.

Dlaczego?

Redux w wersji z użyciem funkcji connect dodaje kilka wydajnościowych optymalizacji, między innymi porównuje zwrotkę z mapStateToProps by uniknąć nadmiarowych rerenderów. Nie są porównywane bezpośrednio referencje (za pomocą operatora ===) a pola na zwracanym obiekcie, czyli w naszym przypadku title i data. Ponieważ jednak jest to porównanie referencji, a obiekt data jest tworzony za każdym wywołaniem funkcji mapStateToProps, każde wywołanie zwróci inną referencję i wymusi rerender.

Podejście z przeniesieniem tworzenia obiektu

By zoptymalizować ilość rerenderów możemy przekazywać pola osobno zamiast w obiekcie, jak to opisane zostało we wspomnianym wcześniej artykule:

Ale nie zawsze będzie to czytelne. Szczególnie jeśli do komponentu chcemy przekazać kilka obiektów, rozbicie ich na pojedyncze wartości wprowadzi chaos i potencjalnie może doprowadzić do konfliktu nazw pól.

Jak temu zaradzić? Przenieść tworzenie obiektu do elementu przekazywanego funkcji connect jako parametr:

const mapStateToProps = state => {
  return {
    user: state.user,
    invoice: state.invoice,
  };
};

const Component = props => {
  const data = {
    name: props.user.name,
    surname: props.user.surname,
    invoice: props.invoice,
  };

  return <DataDisplay title="Less rerenders" data={data}/>;
}

return connect(mapStateToProps)(Component);

Dlaczego to rozwiązanie powoduje mniejszą liczbę rerenderów? Ponieważ jako zwrotkę przekazujemy obiekty ze stanu, do których referencje zmienią się dopiero wtedy, gdy zmienią się znajdujące się w nich wartości. Dopiero wtedy dojdzie do wywołania komponentu Component, który stworzy i przekaże obiekt data do komponentu docelowego co spowoduje oczekiwany rerender.

Działająca aplikacja na Github Pages

Działającą aplikację możesz znaleźć na Github Pages, gdzie sprawdzisz na żywo że ilość rerenderów drugiego przykładu nie zmienia się, gdy zmienisz wartość pola z tekstem:

https://radek-anuszewski.github.io/redux-objects-extended

Pełen kod aplikacji na Github

Pełen kod aplikacji znajduje się pod adresem:

https://github.com/radek-anuszewski/redux-objects-extended

Podsumowanie

Gdy aplikacja nie jest mocno rozbudowana, rerendery mogą nie być problematyczne. Z czasem jednak projekt się rozrasta, dochodzą funkcjonalności, aż któregoś dnia klient zaczyna zgłaszać problemy z wydajnością. Dlatego, pamiętając by nie przesadzać z premature optimization, pilnować tego jak wygląda wydajność aplikacji. Zwłaszcza w świecie Web, gdzie większość ruchu pochodzi z urządzeń mobilnych które znacząco różnią się pomiędzy sobą specyfikacją i możliwościami sprzętowymi.