React Hooks - useEffect, useMemo, useCallback. Kiedy NIE używać i dlaczego o to zapytają cię na rozmowie rekrutacyjnej
"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
- Dlaczego rekruterzy pytają o anti-patterns?
- useEffect - najczęściej nadużywany hook
- useMemo - kiedy memoizacja szkodzi
- useCallback - przedwczesna optymalizacja
- Stale Closure Problem
- React.memo() vs useMemo - różnice
- Zasady hooków - Rules of Hooks
-
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.
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:
- Pamięć - przechowuje poprzedni wynik
- Porównywanie - przy każdym renderowaniu porównuje zależności
- 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ę
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?
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?
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:
- Proste obliczenia - porównywanie zależności jest droższe niż samo obliczenie
- 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
- 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:
- Tablica zależności - efekt uruchomi się tylko gdy zależności się zmienią
- Warunek wewnątrz efektu - sprawdź czy akcja jest potrzebna
- 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:
- useEffect - tylko do efektów ubocznych, nie do transformacji danych
- useMemo - tylko gdy masz zmierzone problemy z wydajnością
- useCallback - tylko z React.memo() lub w zależnościach
- Stale closures - używaj funkcyjnej formy setState lub useRef
- 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ż
- 15 Najtrudniejszych Pytań z React - więcej pytań rekrutacyjnych z React
- 15 Najtrudniejszych Pytań z JavaScript - podstawy JS pod hookami
- Angular vs React - Porównanie dla Programistów - alternatywa dla React
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.
