On our cosmic journey through React, performance is critically important - just like managing resources on a spaceship. Suboptimal use of side effects can lead to excessive re-renders, wasted resources, and sluggish application response. In this module, we'll learn how to optimize useEffect to make our applications run smoothly and efficiently.
The useEffect hook is a powerful tool, but when used improperly it can cause various problems:
The dependency array in useEffect is the key to optimization - it determines when the effect should be re-executed.
1// Problematic code
2function AstronautTracker({ team }) {
3 const [position, setPosition] = useState({ x: 0, y: 0 });
4 const [status, setStatus] = useState('online');
5
6 // This effect will run too frequently
7 useEffect(() => {
8 console.log(`Tracking astronaut: ${team.leader}`);
9 fetchAstronautPosition(team.leader).then(newPosition => {
10 setPosition(newPosition);
11 });
12 }, [team, position, status]); // 🚨 Unnecessary dependencies: position and status
13
14 return <div>Pozycja lidera: {position.x}, {position.y}</div>;
15}1// Corrected code
2function AstronautTracker({ team }) {
3 const [position, setPosition] = useState({ x: 0, y: 0 });
4 const [status, setStatus] = useState('online');
5
6 // Effect uses only necessary dependencies
7 useEffect(() => {
8 console.log(`Tracking astronaut: ${team.leader}`);
9 fetchAstronautPosition(team.leader).then(newPosition => {
10 setPosition(newPosition);
11 });
12 }, [team.leader]); // ✅ Only necessary dependency
13
14 return <div>Pozycja lidera: {position.x}, {position.y}</div>;
15}Objects and functions created during rendering are always "new" from the perspective of dependency comparison. We can optimize this using the useMemo and useCallback hooks:
1function SpaceshipDashboard() {
2 // Without optimization - config object is recreated on every render
3 const config = { speed: 10, fuel: 100 };
4
5 // With optimization - config object is memoized and reused
6 const memoizedConfig = useMemo(() => ({ speed: 10, fuel: 100 }), []);
7
8 // Without optimization - handleData function is recreated on every render
9 const handleData = (data) => console.log(data);
10
11 // With optimization - handleData function is memoized and reused
12 const memoizedHandleData = useCallback((data) => console.log(data), []);
13
14 useEffect(() => {
15 // This effect will run only once
16 const subscription = spaceApi.subscribe(memoizedConfig, memoizedHandleData);
17 return () => subscription.unsubscribe();
18 }, [memoizedConfig, memoizedHandleData]); // Dependencies are stable
19
20 return <div>Spaceship</div>;
21}When an effect reacts to rapidly changing values (e.g., user input), it's worth using debouncing or throttling:
1function SearchAsteroids({ query }) {
2 const [results, setResults] = useState([]);
3 const [debouncedQuery, setDebouncedQuery] = useState(query);
4
5 // Debouncing the query - updates no more than every 500ms
6 useEffect(() => {
7 const timeoutId = setTimeout(() => {
8 setDebouncedQuery(query);
9 }, 500);
10
11 return () => clearTimeout(timeoutId);
12 }, [query]);
13
14 // Search effect runs only when debouncedQuery changes
15 useEffect(() => {
16 if (debouncedQuery) {
17 searchAsteroids(debouncedQuery).then(setResults);
18 }
19 }, [debouncedQuery]);
20
21 return (
22 <div>
23 <h2>Search results</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 // Update only if the component is still mounted
9 if (isMounted) {
10 // Update only if data has actually changed
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}When an effect performs asynchronous operations, "race conditions" can occur - situations where a newer request finishes before an older one:
1// Problematic code - susceptible to race conditions
2function AsteroidDetails({ asteroidId }) {
3 const [details, setDetails] = useState(null);
4
5 useEffect(() => {
6 fetchAsteroidDetails(asteroidId).then(response => {
7 setDetails(response); // 🚨 Possible race condition if asteroidId changes quickly
8 });
9 }, [asteroidId]);
10
11 return <div>{details?.name}</div>;
12}1// Fixed code - protected against race conditions
2function AsteroidDetails({ asteroidId }) {
3 const [details, setDetails] = useState(null);
4
5 useEffect(() => {
6 let isCurrent = true;
7
8 fetchAsteroidDetails(asteroidId).then(response => {
9 // Update state only if this is still the current request
10 if (isCurrent) {
11 setDetails(response);
12 }
13 });
14
15 return () => {
16 isCurrent = false; // Mark that this effect is no longer current
17 };
18 }, [asteroidId]);
19
20 return <div>{details?.name}</div>;
21}For APIs that support AbortController (e.g., fetch), we can actually cancel ongoing requests:
1function PlanetScanner({ sector }) {
2 const [planets, setPlanets] = useState([]);
3 const [loading, setLoading] = useState(false);
4
5 useEffect(() => {
6 setLoading(true);
7
8 // Creating abort controller
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('Error:', error);
21 setLoading(false);
22 }
23 });
24
25 // Cleanup function cancels the request if the effect is interrupted
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}One of the best ways to optimize is creating custom hooks that encapsulate complex logic and can be reused across components:
1// Custom hook for fetching and managing data
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// Using the custom hook
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>Diameter: {data.diameter} km</p>
44 <p>Climate: {data.climate}</p>
45 </div>
46 );
47}In rare cases when you need to perform DOM measurements or synchronize with page layout before rendering, you can use
useLayoutEffect instead of useEffect:1function CockpitInterface() {
2 const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
3 const cockpitRef = useRef(null);
4
5 // useLayoutEffect runs synchronously after all DOM mutations,
6 // but before rendering on screen
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}⚠️ Note:
blocks visual rendering, so it should be used sparingly and only when absolutely necessary.useLayoutEffect
By applying these optimization techniques, your React application will run efficiently even with complex operations and large datasets - just as a well-optimized spaceship can cover vast distances with minimal resource consumption.