9 błędów, które zablokują React Compiler (i jak wykrywa je ESLint)

W poprzednich notkach pisałam już o React Compiler od podstaw — czym jest i jak działa (React Compiler – Kompletny Przewodnik), jak go podglądać w akcji (React Compiler Playground), a także jak pisać kod, który kompilator potrafi zoptymalizować (Jak pisać kod, który React Compiler potrafi zoptymalizować).

Czas zobaczyć, co się dzieje, gdy kod łamie Rules of React.

Przygotowałam w tym celu aplikację demo, którą możecie:

Aplikacja ma dwie sekcje:

  • Good examples – kod, który dawniej wymagał memoizacji, ale React Compiler radzi sobie z nim automatycznie
  • Bad examples – przykłady łamania Rules of React

Jeśli łamiemy te zasady, React Compiler może:

  • zrezygnować z optymalizacji
  • zgłosić błąd ESLint
  • a w niektórych przypadkach nawet zablokować kompilację

Najlepiej widać różnicę w React Developer Tools. W sekcji Good examples wszystkie komponenty są auto-memoizowane ✨. W sekcji Bad examples — React Compiler rezygnuje z optymalizacji tam, gdzie kod łamie Rules of React.


Good examples – kiedy React Compiler robi robotę za nas

Pierwsza część aplikacji pokazuje kod, który dawniej wymagał ręcznej optymalizacji, ale React Compiler potrafi go zoptymalizować automatycznie.

Komponent bez React.memo

Przykład komponentu potomnego:

const ExpensiveChild = ({ count, onIncrement }) => {
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={onIncrement}>Increment count</button>
    </div>
  );
};

W klasycznym React często dodawalibyśmy:

export default React.memo(ExpensiveChild);

Dlaczego?

Ponieważ funkcja przekazywana w propsach zmienia referencję przy każdym renderze.

W naszej aplikacji wygląda to tak:

const handleIncrement = () => {
  setCount(prev => prev + 1);
};

Dawniej bardzo często pojawiał się tu useCallback:

const handleIncrement = useCallback(() => {
  setCount(prev => prev + 1);
}, []);

React Compiler potrafi jednak automatycznie ustabilizować funkcje, więc ręczne useCallback przestaje być potrzebne.

Obliczenia bez useMemo

W aplikacji znajduje się też kosztowna operacja wykonywana podczas renderu:

const filteredItems = items.filter(item => item.toLowerCase().includes(filter.toLowerCase()));
const expensiveCalculation = filteredItems.reduce((acc, item) => {
  let result = 0;  
  for (let i = 0; i < 1000; i++) {
    result += item.length;
  }  
  return acc + result;
}, 0);

W klasycznym React często pojawiłoby się tutaj:

const expensiveCalculation = useMemo(() => {
  ...
}, [filteredItems]);

React Compiler potrafi jednak automatycznie memoizować takie obliczenia, jeśli są czyste i zależą tylko od danych wejściowych.

Bad examples – kiedy łamiemy Rules of React

Druga część aplikacji pokazuje przykłady kodu, który łamie Rules of React.

W takich sytuacjach React Compiler:

  • nie może bezpiecznie zoptymalizować kodu
  • zgłosi błąd ESLint
  • albo całkowicie zablokuje kompilację

Warto wiedzieć, że od wersji 7 eslint-plugin-react-hooks zawiera zarówno klasyczne reguły hooków, jak i reguły React Compilera. Nie potrzebujemy już osobnego eslint-plugin-react-compiler.

Błąd numer 1 – warunkowe wywołanie hooka

if (showCount) {
  const [count] = useState(0);
}

Hooki muszą być wywoływane zawsze w tej samej kolejności.

Nie mogą znajdować się w:

  • if
  • for
  • while
  • funkcjach zagnieżdżonych

Wykrywane przez: react-hooks/rules-of-hooks

Błąd numer 2 – mutowanie propsów

data.value = 100;

Propsy w React powinny być traktowane jako immutable.

Wykrywane przez: react-hooks/immutability

Błąd numer 3 – efekty uboczne podczas renderu

externalStore.data['key'] = value;

Render komponentu powinien być czystą funkcją.

Efekty uboczne powinny znajdować się w useEffect.

Wykrywane przez: react-hooks/purity

Błąd numer 4 – bezpośrednia mutacja stanu

user.age = 26;
setUser(user);

Stan w React powinien być niemutowalny.

Poprawna wersja wyglądałaby tak:

setUser(prev => ({
  ...prev,
  age: 26
}));

Wykrywane przez: react-hooks/immutability.

Błąd numer 5 – hook w pętli

for (let i = 0; i < items.length; i++) {
  useState(items[i]);
}

Hooki nie mogą być wywoływane w pętlach

Wykrywane przez: react-hooks/rules-of-hooks

Błąd numer 6 – hook w funkcji zagnieżdżonej

function createState() {
  const [count] = useState(0);
}

Hooki mogą być wywoływane tylko w:

  • komponentach React
  • custom hookach

Wykrywane przez: react-hooks/rules-of-hooks

Błąd numer 7 – brak zależności w useEffect

useEffect(() => {
  console.log(value);
}, []);

Brakuje value w tablicy zależności. Warto wiedzieć, że React Compiler poprawnie memoizuje ten komponent — sam komponent jest czysty, a błąd dotyczy tylko logiki wewnątrz useEffect, co jest osobną kwestią.

Wykrywane przez: react-hooks/exhaustive-deps

Błąd numer 8 – komponent zdefiniowany w renderze

const InnerComponent = () => {
  return <div>Inner count: {count}</div>;
};

Komponent tworzony przy każdym renderze utrudnia optymalizację.

Wykrywane przez: react-hooks/rules-of-hooks

Błąd numer 9 – mutowanie wartości używanej przez hooki

const [config] = useState({ enabled: true });
config.enabled = false;

Mutowanie wartości zwróconej przez hook łamie założenia React Compiler.

Wykrywane przez: react-hooks/immutability

Podsumowanie przykładów

IssueProblemReguła ESLint
1Conditional hooksreact-hooks/rules-of-hooks
2Props mutationreact-hooks/immutability
3Side effects during renderreact-hooks/purity
4State mutationreact-hooks/immutability
5Hook inside loopreact-hooks/rules-of-hooks
6Hook inside nested functionreact-hooks/rules-of-hooks
7Missing effect dependenciesreact-hooks/exhaustive-deps
8Component inside renderreact-hooks/rules-of-hooks
9Mutating hook valuereact-hooks/immutability

Rola eslint-plugin-react-hooks

Od wersji 7 eslint-plugin-react-hooks zawiera wszystkie potrzebne reguły:

  • react-hooks/rules-of-hooks — błędy numer 1, 5, 6, 8
  • react-hooks/exhaustive-deps — błąd numer 7
  • react-hooks/immutability — błędy numer 2, 4, 9
  • react-hooks/purity — błąd numer 3

Podsumowanie

React Compiler może automatycznie zastąpić wiele rzeczy, które wcześniej robiliśmy ręcznie:

  • React.memo
  • useMemo
  • useCallback

Ale działa najlepiej wtedy, gdy kod jest:

  • zawsze zwraca ten sam wynik dla tych samych danych wejściowych
  • nie mutuje propsów ani stanu bezpośrednio
  • przestrzega Rules of React

Jeśli łamiemy te zasady, kompilator może:

  • zgłosić błąd ESLint
  • zrezygnować z optymalizacji
  • albo zablokować kompilację.

Dlatego jeśli chcecie w pełni wykorzystać możliwości React Compiler, warto upewnić się, że Wasz kod przestrzega tych zasad.

You might also like