React Hooks - useEffect, useMemo, useCallback. Kiedy NIE używać i dlaczego o to zapytają cię na rozmowie rekrutacyjnej

Sławomir Plamowski 12 min czytania

"Kiedy NIE powinieneś używać useMemo?" - to pytanie pada na rozmowach rekrutacyjnych częściej niż mogłoby się wydawać. I większość kandydatów nie zna odpowiedzi. Bo dokumentacja React uczy JAK używać hooków, ale rzadko mówi KIEDY ich unikać. A właśnie to odróżnia juniora od seniora - świadomość, że każde narzędzie ma swój koszt.

Spis treści

  1. Dlaczego rekruterzy pytają o anti-patterns?
  2. useEffect - najczęściej nadużywany hook
  3. useMemo - kiedy memoizacja szkodzi
  4. useCallback - przedwczesna optymalizacja
  5. Stale Closure Problem
  6. React.memo() vs useMemo - różnice
  7. Zasady hooków - Rules of Hooks
  8. Pytania rekrutacyjne z odpowiedziami

    Dlaczego rekruterzy pytają o anti-patterns?

Pytania o anti-patterns hooków React to test dojrzałości programistycznej. Oto co rekruter sprawdza:

1. Rozumienie kosztów Każdy hook ma koszt:

  • useMemo - dodatkowa pamięć + porównywanie zależności
  • useCallback - dodatkowa alokacja + porównywanie
  • useEffect - dodatkowy cykl renderowania

2. Umiejętność profilowania Senior nie zgaduje - mierzy. React DevTools Profiler pokazuje rzeczywiste problemy.

3. Pragmatyzm Przedwczesna optymalizacja to korzeń wszelkiego zła. Lepszy czytelny kod niż "zoptymalizowany" bałagan.

graph TD A[Problem z wydajnością?] -->|Nie| B[Nie optymalizuj] A -->|Tak| C[Zmierz w Profilerze] C --> D{Wąskie gardło?} D -->|Rendering| E[Rozważ React.memo] D -->|Obliczenia| F[Rozważ useMemo] D -->|Callback w deps| G[Rozważ useCallback] B --> H[Pisz czytelny kod]

useEffect - najczęściej nadużywany hook

useEffect jest nadużywany do obliczeń, synchronizacji stanu i transformacji danych. Powinien być używany tylko do efektów ubocznych: fetch danych, subskrypcje, manipulacja DOM. Jeśli możesz coś obliczyć podczas renderowania - zrób to bez useEffect.

Kiedy NIE używać useEffect

1. Transformacja danych do renderowania

// ZŁE - niepotrzebny useEffect
function ProductList({products}) {
    const [filteredProducts, setFilteredProducts] = useState([]);

    useEffect(() => {
        setFilteredProducts(products.filter(p => p.inStock));
    }, [products]);

    return <List items={filteredProducts}/>;
}

// DOBRE - oblicz podczas renderowania
function ProductList({products}) {
    const filteredProducts = products.filter(p => p.inStock);
    return <List items={filteredProducts}/>;
}

2. Resetowanie stanu przy zmianie props

// ZŁE - useEffect do resetowania
function UserProfile({userId}) {
    const [user, setUser] = useState(null);

    useEffect(() => {
        setUser(null); // reset
        fetchUser(userId).then(setUser);
    }, [userId]);
}

// DOBRE - użyj key do wymuszenia remount
<UserProfile key={userId} userId={userId}/>

3. Inicjalizacja stanu z props

// ZŁE
function Counter({initialCount}) {
    const [count, setCount] = useState(0);

    useEffect(() => {
        setCount(initialCount);
    }, []);
}

// DOBRE - inicjalizacja w useState
function Counter({initialCount}) {
    const [count, setCount] = useState(initialCount);
}

Poprawne użycie useEffect

// Fetch danych - TO JEST OK
useEffect(() => {
    let cancelled = false;

    async function fetchData() {
        const response = await fetch(`/api/user/${userId}`);
        if (!cancelled) {
            setUser(await response.json());
        }
    }

    fetchData();

    return () => {
        cancelled = true;
    };
}, [userId]);

// Subskrypcja - TO JEST OK
useEffect(() => {
    const subscription = dataSource.subscribe(handleChange);
    return () => subscription.unsubscribe();
}, [dataSource]);

useMemo - kiedy memoizacja szkodzi

useMemo szkodzi gdy: obliczenie jest proste (szybsze niż porównywanie zależności), zależności zmieniają się przy każdym renderowaniu, lub używasz go "na wszelki wypadek". Memoizacja ma koszt - pamięć i garbage collection.

useMemo zapamiętuje wynik obliczeń między renderowaniami. Ale ma koszt:

  1. Pamięć - przechowuje poprzedni wynik
  2. Porównywanie - przy każdym renderowaniu porównuje zależności
  3. GC pressure - więcej obiektów do sprzątnięcia

Kiedy NIE używać useMemo

1. Proste obliczenia

// ZŁE - koszt memoizacji > koszt obliczenia
const fullName = useMemo(
    () => `${firstName} ${lastName}`,
    [firstName, lastName]
);

// DOBRE - po prostu oblicz
const fullName = `${firstName} ${lastName}`;

2. Zależności zmieniające się zawsze

// ZŁE - nowy obiekt przy każdym renderowaniu
const config = useMemo(
    () => processConfig(options),
    [options] // options = {} każdorazowo = nowy obiekt
);

// Jeśli options tworzone inline, useMemo nic nie daje

3. Premature optimization

// ZŁE - "na wszelki wypadek"
const items = useMemo(
    () => data.map(item => <Item key={item.id} {...item} />),
    [data]
);

// Najpierw zmierz, potem optymalizuj

Kiedy useMemo MA sens

// Kosztowne obliczenia
const sortedData = useMemo(
    () => data.sort((a, b) => complexSort(a, b)),
    [data]
);

// Stabilna referencyjna tożsamość dla useEffect
const config = useMemo(
    () => ({threshold: 100, enabled: true}),
    [] // nigdy się nie zmienia
);

useEffect(() => {
    initializeWithConfig(config);
}, [config]); // teraz config ma stabilną referencję
flowchart TD A[Czy masz problem z wydajnością?] -->|Nie| B[Nie używaj useMemo] A -->|Tak| C[Czy obliczenie jest kosztowne?] C -->|< 1ms| B C -->|> 1ms| D[Czy zależności są stabilne?] D -->|Nie| E[Najpierw ustabilizuj zależności] D -->|Tak| F[Użyj useMemo]

useCallback - przedwczesna optymalizacja

useCallback zapobiega tworzeniu nowej funkcji przy każdym renderowaniu. Ale ma sens TYLKO gdy: przekazujesz funkcję do React.memo() komponentu, lub funkcja jest zależnością useEffect. W innych przypadkach to przedwczesna optymalizacja.

useCallback to useMemo dla funkcji:

// Te dwa zapisy są równoważne
const memoizedFn = useCallback(() => doSomething(a, b), [a, b]);
const memoizedFn = useMemo(() => () => doSomething(a, b), [a, b]);

Kiedy useCallback jest BEZUŻYTECZNY

1. Komponent dziecko nie jest memoizowany

// BEZUŻYTECZNE - Button nie jest React.memo
function Parent() {
    const handleClick = useCallback(() => {
        console.log('clicked');
    }, []);

    return <Button onClick={handleClick}/>;
}

// Button renderuje się przy każdym renderowaniu Parent
// czy handleClick ma nową referencję czy nie - bez znaczenia

2. Funkcja używana tylko lokalnie

// BEZUŻYTECZNE
function Form() {
    const validate = useCallback((value) => {
        return value.length > 0;
    }, []);

    // validate używane tylko tutaj, nie przekazywane dalej
    const handleSubmit = () => {
        if (validate(input)) { ...
        }
    };
}

Kiedy useCallback MA sens

1. Z React.memo()

const ExpensiveChild = React.memo(function ExpensiveChild({onClick}) {
    console.log('ExpensiveChild rendered');
    return <button onClick={onClick}>Click</button>;
});

function Parent() {
    // SENS - stabilizuje referencję dla memo komponentu
    const handleClick = useCallback(() => {
        console.log('clicked');
    }, []);

    return <ExpensiveChild onClick={handleClick}/>;
}

2. W zależnościach useEffect

function SearchComponent({query}) {
    const fetchResults = useCallback(async () => {
        const response = await fetch(`/api/search?q=${query}`);
        return response.json();
    }, [query]);

    useEffect(() => {
        fetchResults().then(setResults);
    }, [fetchResults]); // stabilna zależność
}

Stale Closure Problem

Stale closure to bug gdy useEffect lub useCallback używa nieaktualnych wartości z poprzedniego renderowania. Dzieje się tak gdy tablica zależności jest niekompletna. Rozwiązanie: zawsze dodawaj wszystkie używane zmienne do tablicy zależności.

JavaScript closures "zamykają" zmienne z momentu utworzenia funkcji. W React może to powodować problemy:

// BUG - stale closure
function Counter() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        const id = setInterval(() => {
            console.log(count); // zawsze 0!
            setCount(count + 1); // zawsze 0 + 1 = 1
        }, 1000);

        return () => clearInterval(id);
    }, []); // pusta tablica - count "zamknięty" na 0
}

Dlaczego to się dzieje?

sequenceDiagram participant R as React participant E as useEffect participant I as setInterval R ->> E: Pierwszy render, count=0 E ->> I: Tworzy interval z count=0 R ->> R: setCount(1), count=1 Note over E, I: Interval nadal widzi count=0 I ->> I: console.log(0)

Rozwiązania

1. Dodaj zależność (może powodować reset intervalu)

useEffect(() => {
    const id = setInterval(() => {
        setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
}, [count]); // interval resetowany przy każdej zmianie

2. Użyj funkcyjnej formy setState

useEffect(() => {
    const id = setInterval(() => {
        setCount(prev => prev + 1); // nie zależy od count
    }, 1000);
    return () => clearInterval(id);
}, []); // bezpieczna pusta tablica

3. Użyj useRef dla aktualnej wartości

function Counter() {
    const [count, setCount] = useState(0);
    const countRef = useRef(count);

    useEffect(() => {
        countRef.current = count; // aktualizuj ref
    }, [count]);

    useEffect(() => {
        const id = setInterval(() => {
            console.log(countRef.current); // zawsze aktualna
        }, 1000);
        return () => clearInterval(id);
    }, []);
}

React.memo() vs useMemo - różnice

React.memo() memoizuje komponent - zapobiega re-renderowaniu gdy props się nie zmieniły. useMemo memoizuje wartość - zapobiega ponownemu obliczaniu gdy zależności się nie zmieniły. React.memo działa na poziomie komponentu, useMemo na poziomie wartości.

To dwa różne narzędzia do różnych problemów:

Cecha React.memo() useMemo
Co memoizuje Cały komponent Wartość/obliczenie
Gdzie używamy Owijamy definicję komponentu Wewnątrz komponentu
Porównuje Props (shallow) Zależności (shallow)
Cel Pominięcie renderowania Pominięcie obliczenia

React.memo() w praktyce

// Bez memo - renderuje się przy każdym renderowaniu rodzica
function Tweet({author, content, likes}) {
    console.log('Tweet rendered');
    return (
        <div>
            <p>{content}</p>
            <span>By {author} | {likes} likes</span>
        </div>
    );
}

// Z memo - renderuje się tylko gdy props się zmienią
const Tweet = React.memo(function Tweet({author, content, likes}) {
    console.log('Tweet rendered');
    return (
        <div>
            <p>{content}</p>
            <span>By {author} | {likes} likes</span>
        </div>
    );
});

useMemo w praktyce

function TweetList({tweets, filter}) {
    // Bez useMemo - filtruje przy każdym renderowaniu
    const filteredTweets = tweets.filter(t => t.includes(filter));

    // Z useMemo - filtruje tylko gdy tweets lub filter się zmieni
    const filteredTweets = useMemo(
        () => tweets.filter(t => t.includes(filter)),
        [tweets, filter]
    );

    return filteredTweets.map(t => <Tweet key={t.id} {...t} />);
}

Kiedy używać czego?

flowchart TD A[Problem z wydajnością] --> B{Co jest wolne?} B -->|Komponent się re - renderuje| C[React.memo na komponencie] B -->|Obliczenie wewnątrz| D[useMemo na obliczeniu] C --> E{Props to funkcje?} E -->|Tak| F[Dodaj useCallback w rodzicu] E -->|Nie| G[React.memo wystarczy]

Zasady hooków - Rules of Hooks

Dwie zasady: 1) Wywołuj hooki tylko na najwyższym poziomie (nie w pętlach, warunkach, zagnieżdżonych funkcjach), 2) Wywołuj tylko w komponentach funkcyjnych lub własnych hookach. Te zasady pozwalają Reactowi poprawnie śledzić stan między renderowaniami.

React polega na kolejności wywołań hooków - musi być identyczna przy każdym renderowaniu.

Zasada 1: Tylko na najwyższym poziomie

// ZŁE - hook w warunku
function Form({isEditing}) {
    if (isEditing) {
        const [name, setName] = useState(''); // może nie zostać wywołany!
    }
    const [email, setEmail] = useState('');
}

// DOBRE - warunek wewnątrz hooka lub po hookach
function Form({isEditing}) {
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');

    if (!isEditing) {
        return <p>View mode</p>;
    }
    // ...
}

Dlaczego kolejność ma znaczenie?

React identyfikuje hooki po pozycji, nie po nazwie:

// Pierwsze renderowanie:
useState('Mary')  // 1. Stan: 'Mary'
useEffect(...)    // 2. Efekt
useState('Poppins') // 3. Stan: 'Poppins'

// Drugie renderowanie (musi być identyczne):
useState('Mary')  // 1. Odczytaj stan: 'Mary'
useEffect(...)    // 2. Zamień efekt
useState('Poppins') // 3. Odczytaj stan: 'Poppins'

Jeśli hook jest w warunku, kolejność może się zmienić i React przypisze zły stan do złej zmiennej.

Zasada 2: Tylko w komponentach React lub własnych hookach

// ZŁE - hook w zwykłej funkcji
function validateEmail(email) {
    const [isValid, setIsValid] = useState(false); // error!
    // ...
}

// DOBRE - własny hook (nazwa zaczyna się od "use")
function useEmailValidation(email) {
    const [isValid, setIsValid] = useState(false);

    useEffect(() => {
        setIsValid(email.includes('@'));
    }, [email]);

    return isValid;
}

Pytania rekrutacyjne z odpowiedziami

Pytanie 1: "Opisz sytuację, w której useMemo pogorszy wydajność"

"useMemo pogorszy wydajność w trzech przypadkach:

  1. Proste obliczenia - porównywanie zależności jest droższe niż samo obliczenie
  2. Niestabilne zależności - gdy obiekt/tablica tworzona inline zmienia referencję przy każdym renderowaniu, useMemo i tak ponownie obliczy wartość, ale dodatkowo wykona porównanie
  3. Duże obiekty - memoizacja trzyma poprzednią wartość w pamięci, co może być problemem przy dużych strukturach danych

Najpierw mierzę w Profilerze, potem optymalizuję."

Pytanie 2: "Jak naprawiłbyś stale closure w useEffect?"

// Problem
useEffect(() => {
    const id = setInterval(() => {
        setCount(count + 1); // stale closure
    }, 1000);
    return () => clearInterval(id);
}, []);

// Rozwiązanie 1: Funkcyjna forma setState
useEffect(() => {
    const id = setInterval(() => {
        setCount(prev => prev + 1);
    }, 1000);
    return () => clearInterval(id);
}, []);

// Rozwiązanie 2: useRef dla aktualnej wartości
const countRef = useRef(count);
useEffect(() => {
    countRef.current = count;
}, [count]);

Pytanie 3: "Kiedy useCallback jest bezużyteczny?"

"useCallback jest bezużyteczny gdy:

  • Komponent przyjmujący funkcję nie jest opakowany w React.memo()
  • Funkcja nie jest przekazywana do dziecka, tylko używana lokalnie
  • Funkcja nie jest w tablicy zależności useEffect

W tych przypadkach to przedwczesna optymalizacja, która tylko komplikuje kod."

Pytanie 4: "Czym różni się przekazanie [] vs brak drugiego argumentu w useEffect?"

// Pusta tablica - uruchom RAZ przy montowaniu
useEffect(() => {
    console.log('mounted');
    return () => console.log('unmounted');
}, []);

// Brak argumentu - uruchom przy KAŻDYM renderowaniu
useEffect(() => {
    console.log('rendered');
});

"Pusta tablica to odpowiednik componentDidMount + componentWillUnmount. Brak argumentu to componentDidMount + componentDidUpdate + componentWillUnmount."

Pytanie 5: "Jak pominąć niepotrzebne wywołania useEffect?"

"Trzy sposoby:

  1. Tablica zależności - efekt uruchomi się tylko gdy zależności się zmienią
  2. Warunek wewnątrz efektu - sprawdź czy akcja jest potrzebna
  3. Właściwa granularność - rozbij na mniejsze efekty z różnymi zależnościami
// Zależności
useEffect(() => {
  document.title = `Clicked ${count} times!`;
}, [count]); // tylko gdy count się zmieni

// Warunek wewnątrz
useEffect(() => {
  if (user.id !== prevUserId) {
    fetchProfile(user.id);
  }
}, [user.id]);

Podsumowanie

React Hooks to potężne narzędzie, ale jak każde narzędzie - może być nadużywane. Kluczowe wnioski:

  1. useEffect - tylko do efektów ubocznych, nie do transformacji danych
  2. useMemo - tylko gdy masz zmierzone problemy z wydajnością
  3. useCallback - tylko z React.memo() lub w zależnościach
  4. Stale closures - używaj funkcyjnej formy setState lub useRef
  5. Rules of Hooks - hooki na najwyższym poziomie, zawsze w tej samej kolejności

Pamiętaj: przedwczesna optymalizacja to korzeń wszelkiego zła. Najpierw pisz czytelny kod, potem mierz, a dopiero potem optymalizuj tam, gdzie jest to naprawdę potrzebne.


Zobacz też


Sprawdź swoją wiedzę z React Hooks

Chcesz przećwiczyć więcej pytań o React Hooks przed rozmową rekrutacyjną?

Nasze fiszki React zawierają 50+ pytań o hooki, ich wzorce użycia i częste błędy. Każda fiszka ma szczegółową odpowiedź dostosowaną do poziomu Junior, Regular i Senior.

Zobacz fiszki React online - natychmiastowy dostęp, nauka w przeglądarce, bez instalacji.

Chcesz więcej pytań rekrutacyjnych?

To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.