Poznajemy hook useMemo i zastanawiamy się, kiedy go używać. Do dzieła.

Ok, przypomnijmy sobie co się dzieje przy każdym renderze RFC:

  • Każdy console log wewnątrz tego RFC się odpali (przydatne do debugowania)
  • Każda stała i zmienna zostanie utworzona od nowa i od nowa przypisana jej wartość, nawet taka sama
  • Każda funkcja zostanie zdefiniowana od nowa, nawet jeżeli taka sama i będzie miała nową referencję
  • W związku z tym każda funkcja niebędąca funkcją setState czy inną z hooków musi być dopisywana do tablic zależności, ale to mniejsza

Mam nadzieję, że pamiętamy też mount render, diff rendery, remount, key, memo i tak dalej. Ok, funkcje createTodos i filterTodos:

export function createTodos() {
  const todos = [];
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: "Todo " + (i + 1),
      completed: Math.random() > 0.5
    });
  }
  return todos;
}

export function filterTodos(todos, tab) {
  console.log('[ARTIFICIALLY SLOW] Filtering ' + todos.length + ' todos for "' + tab + '" tab.');
  let startTime = performance.now();
  while (performance.now() - startTime < 500) {
    // Do nothing for 500 ms to emulate extremely slow code
  }

  return todos.filter(todo => {
    if (tab === 'all') {
      return true;
    } else if (tab === 'active') {
      return !todo.completed;
    } else if (tab === 'completed') {
      return todo.completed;
    }
  });
}

Ok, RFC App, przykład z dokumentacji Reacta btw:

import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';

const todos = createTodos();

export default function App() {
  const [tab, setTab] = useState('all');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <button onClick={() => setTab('all')}>
        All
      </button>
      <button onClick={() => setTab('active')}>
        Active
      </button>
      <button onClick={() => setTab('completed')}>
        Completed
      </button>
      <br />
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <hr />
      <TodoList
        todos={todos}
        tab={tab}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

Swoją drogą powyżej RFC mamy wywołanie createTodos i przypisanie do stałej todos, zatem to nie będzie się wykonywać co render, to już jakaś optymalizacja jest.

Mamy jednak coś takiego jak stan isDark. Zmiana stanu wywoła diff-render tego RFC i diff-rendery wszystkich jego dzieci.

To może zabezpieczymy te dzieci przez memo? No zabezpieczymy, ale im isDark jest jako prop przekazywany! I one wtedy mają mieć diff-render i zmienić tryb dzienny na nocny albo odwrotnie.

To co powoduje problem? To:

import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      <ul>
        <p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ?
              <s>{todo.text}</s> :
              todo.text
            }
          </li>
        ))}
      </ul>
    </div>
  );
}

Chcemy mieć diff-render dziecka gdy zmieni się theme, natomiast zauważmy, że zmiana theme i idący za nią diff-render powoduje stworzenie od zera stałej visibleTodos i przypisanie do niej wywołania filterTodos, która wykonuje ciężkie, długie i zupełnie niepotrzebne obliczene.

Niepotrzebne, bo zmienił się kolor tła, tryb się zmienił, zaś filtrowane todosy nie, pokazuje te same, ale niestety oblicza coś, co ma już obliczone. Bo jakkolwiek tego filtra nie mieliśmy ustawionego, zmiana trybu na nocny nie zmieniła go.

Nie zmieniła go, ale diff-render rodzica i dziecka wywołał ponowne obliczenie. Obliczenie, które chcemy keszować:

import { useMemo } from 'react';
import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  return (
    <div className={theme}>
      <p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>
      <ul>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ?
              <s>{todo.text}</s> :
              todo.text
            }
          </li>
        ))}
      </ul>
    </div>
  );
}

Ten hook też ma tablicę zależności. Teraz, warto to sobie uświadomić:

  • Każdy diff-render rodzica wywoła diff-render dziecka TodoList
  • Ustawienie memo na RFC TodoList nie zmieniłoby niczego, bo nie ma takich diff-renderów rodzica, które nie zmieniają propsów dziecka TodoList
  • Gdy rodzic zmieni todos albo tab, ma się wykonać obliczenie filterTodos i ma być keszowane do visibleTodos
  • Gdy rodzic zmieni theme, które jest przekazywane jako props, to rodzic ma diff render i dziecko ma diff render i zmienia swoje kolorki, ale nie wykonuje niepotrzebnego i bardzo obciążającego obliczenia dla visibleTodos, które są takie same, kolor tła im się tylko zmienia

I do takich rzeczy używamy useMemo. Nie używamy natomiast go do logiki. Typu, mamy jakieś diff rendery nie z rodzica, ale ze środka tego komponentu. I one np. zmieniają styl, zmieniają kolory odpowiedzi w quizie. A na top-level mamy ustawione mieszanie odpowiedzi quiza. I jako akcja RFC jest odpalana na każdy render.

Wtedy z pobudek logicznych a nie wydajności możemy chcieć, aby pomieszanie kolejności odpowiedzi odbywało się tylko na mount render. I możemy się z useMemo bawić, ale pamiętajmy – ono służy memoizacji obliczeń, które są ciężkie a nie muszą być zawsze obliczane co każdy render.

W związku z tym używanie useMemo ponosi za sobą też jakiś koszt wydajności i jest po to, aby był profit, nie strata. Zatem useMemo nie służy do sterowania logiką. Służy tylko do jednego i cel ten poznaliśmy.