We use cookies to enhance your experience on the site
CodeWorlds

Performance Optimization with useEffect

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.

Performance issues with useEffect

The useEffect hook is a powerful tool, but when used improperly it can cause various problems:

  1. Effects running too frequently - when dependencies change often or are improperly defined
  2. Expensive operations - when performing complex calculations or DOM manipulations inside effects
  3. Cascading updates - when one effect causes state changes that trigger subsequent effects
  4. Unnecessary re-renders - when effects update state even though the data hasn't actually changed

Optimizing the dependency array

The dependency array in useEffect is the key to optimization - it determines when the effect should be re-executed.

Error 1: Too many dependencies

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}

Solution: Use only necessary dependencies

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}

Technique: Memoizing objects and functions

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}

Techniques for optimizing expensive operations

1. Debouncing and throttling

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}

2. Avoiding unnecessary state updates

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}

Optimizing effects with asynchronous data

Problem: Race conditions

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}

Solution: Canceling stale requests

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}

Advanced technique: AbortController

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}

Separating logic into custom hooks

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}

Using the layoutEffect mechanism

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:

useLayoutEffect
blocks visual rendering, so it should be used sparingly and only when absolutely necessary.

Summary: Best Practices for Optimizing useEffect

  1. Minimize dependencies - use only necessary values in the dependency array
  2. Separate effects - instead of one complex effect, use several simpler ones
  3. Memoize dependencies - use useMemo and useCallback for objects and functions
  4. Use debouncing/throttling - for rapidly changing values
  5. Cancel outdated async operations - avoid race conditions and memory leaks
  6. Create custom hooks - encapsulate complex logic in reusable hooks
  7. Analyze rendering patterns - use React DevTools to detect excessive renders
  8. Avoid infinite loops - always make sure effects don't cause infinite update cycles

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.

Go to CodeWorlds