W kosmicznej podróży przez React, wydajność jest krytycznie ważna - podobnie jak zarządzanie zasobami na statku kosmicznym. Nieoptymalne wykorzystanie efektów ubocznych może prowadzić do nadmiernej liczby renderowań, marnotrawstwa zasobów i wolnej reakcji aplikacji. W tym module nauczymy się, jak optymalizować działanie useEffect, aby nasze aplikacje działały płynnie i efektywnie.
Hook useEffect jest potężnym narzędziem, ale niewłaściwie używany może powodować różne problemy:
Tablica zależności w useEffect jest kluczem do optymalizacji - określa kiedy efekt powinien być ponownie wykonany.
1// Problematyczny kod
2function AstronautTracker({ team }) {
3 const [position, setPosition] = useState({ x: 0, y: 0 });
4 const [status, setStatus] = useState('online');
5
6 // Ten efekt będzie uruchamiany zbyt często
7 useEffect(() => {
8 console.log(`Śledzenie astronauty: ${team.leader}`);
9 fetchAstronautPosition(team.leader).then(newPosition => {
10 setPosition(newPosition);
11 });
12 }, [team, position, status]); // 🚨 Zbędne zależności: position i status
13
14 return <div>Pozycja lidera: {position.x}, {position.y}</div>;
15}1// Poprawiony kod
2function AstronautTracker({ team }) {
3 const [position, setPosition] = useState({ x: 0, y: 0 });
4 const [status, setStatus] = useState('online');
5
6 // Efekt używa tylko potrzebnych zależności
7 useEffect(() => {
8 console.log(`Śledzenie astronauty: ${team.leader}`);
9 fetchAstronautPosition(team.leader).then(newPosition => {
10 setPosition(newPosition);
11 });
12 }, [team.leader]); // ✅ Tylko potrzebna zależność
13
14 return <div>Pozycja lidera: {position.x}, {position.y}</div>;
15}Obiekty i funkcje tworzone podczas renderowania są zawsze "nowe" z perspektywy porównywania zależności. Możemy to zoptymalizować za pomocą hooków useMemo i useCallback:
1function SpaceshipDashboard() {
2 // Bez optymalizacji - obiekt config jest tworzony na nowo przy każdym renderze
3 const config = { speed: 10, fuel: 100 };
4
5 // Z optymalizacją - obiekt config jest zapamiętany i reużyty
6 const memoizedConfig = useMemo(() => ({ speed: 10, fuel: 100 }), []);
7
8 // Bez optymalizacji - funkcja handleData jest tworzona na nowo przy każdym renderze
9 const handleData = (data) => console.log(data);
10
11 // Z optymalizacją - funkcja handleData jest zapamiętana i reużyta
12 const memoizedHandleData = useCallback((data) => console.log(data), []);
13
14 useEffect(() => {
15 // Ten efekt będzie uruchamiany tylko raz
16 const subscription = spaceApi.subscribe(memoizedConfig, memoizedHandleData);
17 return () => subscription.unsubscribe();
18 }, [memoizedConfig, memoizedHandleData]); // Zależności są stabilne
19
20 return <div>Statek kosmiczny</div>;
21}Gdy efekt reaguje na szybko zmieniające się wartości (np. input użytkownika), warto użyć debouncing lub throttling:
1function SearchAsteroids({ query }) {
2 const [results, setResults] = useState([]);
3 const [debouncedQuery, setDebouncedQuery] = useState(query);
4
5 // Debouncing zapytania - aktualizuje się nie częściej niż co 500ms
6 useEffect(() => {
7 const timeoutId = setTimeout(() => {
8 setDebouncedQuery(query);
9 }, 500);
10
11 return () => clearTimeout(timeoutId);
12 }, [query]);
13
14 // Efekt wyszukiwania wykonuje się tylko gdy debouncedQuery się zmieni
15 useEffect(() => {
16 if (debouncedQuery) {
17 searchAsteroids(debouncedQuery).then(setResults);
18 }
19 }, [debouncedQuery]);
20
21 return (
22 <div>
23 <h2>Wyniki wyszukiwania</h2>
24 <ul>
25 {results.map(asteroid => (
26 <li key={asteroid.id}>{asteroid.name}</li>
27 ))}
28 </ul>
29 </div>
30 );
31}1function SpaceStation({ stationId }) {
2 const [data, setData] = useState(null);
3
4 useEffect(() => {
5 let isMounted = true;
6
7 fetchStationData(stationId).then(newData => {
8 // Aktualizuj tylko jeśli komponent jest wciąż zamontowany
9 if (isMounted) {
10 // Aktualizuj tylko jeśli dane faktycznie się zmieniły
11 if (JSON.stringify(data) !== JSON.stringify(newData)) {
12 setData(newData);
13 }
14 }
15 });
16
17 return () => {
18 isMounted = false;
19 };
20 }, [stationId]);
21
22 return data ? <StationDisplay data={data} /> : <Loading />;
23}Gdy efekt wykonuje asynchroniczne operacje, może dojść do "wyścigów" - sytuacji, gdzie nowszy request zakończy się przed starszym:
1// Problematyczny kod - podatny na wyścigi
2function AsteroidDetails({ asteroidId }) {
3 const [details, setDetails] = useState(null);
4
5 useEffect(() => {
6 fetchAsteroidDetails(asteroidId).then(response => {
7 setDetails(response); // 🚨 Możliwy wyścig, jeśli asteroidId zmieni się szybko
8 });
9 }, [asteroidId]);
10
11 return <div>{details?.name}</div>;
12}1// Poprawiony kod - zabezpieczony przed wyścigami
2function AsteroidDetails({ asteroidId }) {
3 const [details, setDetails] = useState(null);
4
5 useEffect(() => {
6 let isCurrent = true;
7
8 fetchAsteroidDetails(asteroidId).then(response => {
9 // Aktualizuj stan tylko jeśli to wciąż aktualny request
10 if (isCurrent) {
11 setDetails(response);
12 }
13 });
14
15 return () => {
16 isCurrent = false; // Oznacz, że ten efekt nie jest już aktualny
17 };
18 }, [asteroidId]);
19
20 return <div>{details?.name}</div>;
21}Dla API, które wspierają AbortController (np. fetch), możemy faktycznie anulować trwające requesty:
1function PlanetScanner({ sector }) {
2 const [planets, setPlanets] = useState([]);
3 const [loading, setLoading] = useState(false);
4
5 useEffect(() => {
6 setLoading(true);
7
8 // Tworzenie kontrolera anulowania
9 const controller = new AbortController();
10 const signal = controller.signal;
11
12 fetch(`https://api.space/planets?sector=${sector}`, { signal })
13 .then(response => response.json())
14 .then(data => {
15 setPlanets(data);
16 setLoading(false);
17 })
18 .catch(error => {
19 if (error.name !== 'AbortError') {
20 console.error('Błąd:', error);
21 setLoading(false);
22 }
23 });
24
25 // Funkcja czyszcząca anuluje request, jeśli efekt zostanie przerwany
26 return () => {
27 controller.abort();
28 };
29 }, [sector]);
30
31 return (
32 <div>
33 {loading ? <Loading /> : (
34 <PlanetsList planets={planets} />
35 )}
36 </div>
37 );
38}Jednym z najlepszych sposobów optymalizacji jest tworzenie własnych hooków, które enkapsulują złożoną logikę i mogą być reużyte między komponentami:
1// Własny hook do pobierania i zarządzania danymi
2function usePlanetData(planetId) {
3 const [data, setData] = useState(null);
4 const [loading, setLoading] = useState(true);
5 const [error, setError] = useState(null);
6
7 useEffect(() => {
8 let isMounted = true;
9 setLoading(true);
10
11 fetchPlanetData(planetId)
12 .then(result => {
13 if (isMounted) {
14 setData(result);
15 setLoading(false);
16 }
17 })
18 .catch(err => {
19 if (isMounted) {
20 setError(err);
21 setLoading(false);
22 }
23 });
24
25 return () => {
26 isMounted = false;
27 };
28 }, [planetId]);
29
30 return { data, loading, error };
31}
32
33// Korzystanie z własnego hooka
34function PlanetProfile({ planetId }) {
35 const { data, loading, error } = usePlanetData(planetId);
36
37 if (loading) return <LoadingIndicator />;
38 if (error) return <ErrorMessage error={error} />;
39
40 return (
41 <div>
42 <h1>{data.name}</h1>
43 <p>Średnica: {data.diameter} km</p>
44 <p>Klimat: {data.climate}</p>
45 </div>
46 );
47}W rzadkich przypadkach, gdy potrzebujesz wykonać pomiary DOM lub synchronizować się z układem strony przed renderowaniem, możesz użyć
useLayoutEffect zamiast useEffect:1function CockpitInterface() {
2 const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
3 const cockpitRef = useRef(null);
4
5 // useLayoutEffect uruchamia się synchronicznie po wszystkich mutacjach DOM,
6 // ale przed renderowaniem na ekranie
7 useLayoutEffect(() => {
8 if (cockpitRef.current) {
9 const { width, height } = cockpitRef.current.getBoundingClientRect();
10 setDimensions({ width, height });
11 }
12 }, []);
13
14 return (
15 <div ref={cockpitRef} className="cockpit">
16 <ControlPanel width={dimensions.width} height={dimensions.height} />
17 </div>
18 );
19}⚠️ Uwaga:
blokuje renderowanie wizualne, więc powinien być używany oszczędnie i tylko gdy absolutnie konieczne.useLayoutEffect
Stosując te techniki optymalizacji, twoja aplikacja React będzie działać efektywnie nawet przy złożonych operacjach i dużych zbiorach danych - podobnie jak dobrze zoptymalizowany statek kosmiczny może pokonywać ogromne odległości z minimalnym zużyciem zasobów.