JavaScript and React course Β· Module 5: useEffect and Custom Hooks

Performance Optimization with useEffect

7 min read
In this lesson7

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.

Code for this lesson: App.jsx
1import React, { useState, useEffect, useRef, useCallback } from 'react';
2
3// Demonstrating the dependency array
4function DependencyArrayDemo() {
5  const [count, setCount] = useState(0);
6  const [name, setName] = useState('');
7  const [logs, setLogs] = useState([]);
8
9  const addLog = (msg) => {
10    setLogs(prev => [...prev.slice(-4), `[${new Date().toLocaleTimeString()}] ${msg}`]);
11  };
12
13  // Effect without dependencies - runs on every render
14  const effectRuns = useRef(0);
15  useEffect(() => {
16    // No setState here: a state change would trigger another render and an infinite loop
17    effectRuns.current += 1;
18    console.log(`Effect WITHOUT a dependency array (${effectRuns.current})`);
19  });
20
21  // Effect with an empty array - only on mount
22  useEffect(() => {
23    addLog('Effect with [] - only on mount');
24  }, []);
25
26  // Effect with a dependency on count
27  useEffect(() => {
28    addLog(`Effect with [count] - count = ${count}`);
29  }, [count]);
30
31  // Effect with a dependency on name
32  useEffect(() => {
33    if (name) addLog(`Effect with [name] - name = "${name}"`);
34  }, [name]);
35
36  return (
37    <div className="demo-card">
38      <h2>Dependency array</h2>
39      <div className="inputs">
40        <button onClick={() => setCount(c => c + 1)}>
41          Count: {count}
42        </button>
43        <input
44          value={name}
45          onChange={(e) => setName(e.target.value)}
46          placeholder="Enter a name..."
47        />
48      </div>
49      <div className="logs">
50        {logs.map((log, i) => <div key={i}>{log}</div>)}
51      </div>
52    </div>
53  );
54}
55
56// Optimizing fetch requests
57function OptimizedFetchDemo() {
58  const [searchTerm, setSearchTerm] = useState('');
59  const [results, setResults] = useState([]);
60  const [loading, setLoading] = useState(false);
61  const [fetchCount, setFetchCount] = useState(0);
62  const abortControllerRef = useRef(null);
63
64  // Debounced search with cleanup
65  useEffect(() => {
66    // Cancel the previous request
67    if (abortControllerRef.current) {
68      abortControllerRef.current.abort();
69    }
70
71    if (!searchTerm.trim()) {
72      setResults([]);
73      return;
74    }
75
76    // Debounce - wait 500ms before searching
77    const timeoutId = setTimeout(() => {
78      abortControllerRef.current = new AbortController();
79
80      setLoading(true);
81      setFetchCount(c => c + 1);
82
83      // Simulating an API call
84      setTimeout(() => {
85        const mockResults = [
86          `Planet: ${searchTerm}-Prime`,
87          `Star: ${searchTerm}-Alpha`,
88          `Nebula: ${searchTerm}-Nebula`,
89        ];
90        setResults(mockResults);
91        setLoading(false);
92      }, 500);
93    }, 500);
94
95    // Cleanup - cancel the timeout and request
96    return () => {
97      clearTimeout(timeoutId);
98      if (abortControllerRef.current) {
99        abortControllerRef.current.abort();
100      }
101    };
102  }, [searchTerm]);
103
104  return (
105    <div className="demo-card">
106      <h2>Fetch Optimization</h2>
107      <div className="fetch-counter">
108        Requests made: <span>{fetchCount}</span>
109      </div>
110      <input
111        value={searchTerm}
112        onChange={(e) => setSearchTerm(e.target.value)}
113        placeholder="Search for space objects..."
114      />
115      <div className="results">
116        {loading ? (
117          <div className="loading">Searching...</div>
118        ) : results.length > 0 ? (
119          results.map((result, i) => (
120            <div key={i} className="result">{result}</div>
121          ))
122        ) : searchTerm ? (
123          <div className="empty">Type to search</div>
124        ) : null}
125      </div>
126      <p className="hint">Debounce 500ms + AbortController</p>
127    </div>
128  );
129}
130
131// Avoiding infinite loops
132function InfiniteLoopPreventionDemo() {
133  const [data, setData] = useState({ count: 0 });
134  const renderCount = useRef(0);
135  const [safeMode, setSafeMode] = useState(true);
136
137  // Counting renders
138  renderCount.current += 1;
139
140  // DANGEROUS - would cause an infinite loop
141  // useEffect(() => {
142  //   setData({ count: data.count }); // New object = new reference = re-render
143  // }, [data]);
144
145  // SAFE - we use a callback
146  const incrementSafe = useCallback(() => {
147    setData(prev => ({ count: prev.count + 1 }));
148  }, []);
149
150  return (
151    <div className="demo-card">
152      <h2>Avoiding loops</h2>
153      <div className="stats">
154        <div className="stat">
155          <span className="label">Count:</span>
156          <span className="value">{data.count}</span>
157        </div>
158        <div className="stat">
159          <span className="label">Renders:</span>
160          <span className="value">{renderCount.current}</span>
161        </div>
162      </div>
163      <button onClick={incrementSafe}>
164        Safe increment (useCallback)
165      </button>
166      <p className="hint">
167        Use an updater function instead of a direct reference
168      </p>
169    </div>
170  );
171}
172
173// Optimization with useRef instead of state
174function UseRefOptimizationDemo() {
175  const [, forceRender] = useState(0);
176  const clickCountRef = useRef(0);
177  const renderCountRef = useRef(0);
178
179  renderCountRef.current++;
180
181  const handleClick = () => {
182    clickCountRef.current++;
183    // Doesn't cause a re-render!
184  };
185
186  return (
187    <div className="demo-card">
188      <h2>useRef vs useState</h2>
189      <div className="comparison">
190        <div className="box">
191          <h3>useRef (without re-render)</h3>
192          <p>Clicks: {clickCountRef.current}</p>
193          <button onClick={handleClick}>Click (useRef)</button>
194        </div>
195        <div className="box">
196          <h3>Component renders</h3>
197          <p>Count: {renderCountRef.current}</p>
198          <button onClick={() => forceRender(n => n + 1)}>
199            Force render
200          </button>
201        </div>
202      </div>
203      <p className="hint">
204        useRef doesn't cause a re-render when the value changes
205      </p>
206    </div>
207  );
208}
209
210function App() {
211  return (
212    <div className="app">
213      <h1>Optimizing useEffect</h1>
214      <div className="demos">
215        <DependencyArrayDemo />
216        <OptimizedFetchDemo />
217        <InfiniteLoopPreventionDemo />
218        <UseRefOptimizationDemo />
219      </div>
220    </div>
221  );
222}
223
224export default App;

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What should the cleanup function in useEffect return?

  2. 2. Why can placing an object directly in the useEffect dependency array cause an infinite loop?

These are 2 of 5 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Vertical ordering

    Arrange the React rendering steps with useEffect in order:

  • Code editor

    Animation Frame Management Hook

  • Code editor

    Debounced Search Implementation

  • Code editor

    Create a TemperatureMonitor component using useEffect to simulate temperature readings every 2 seconds.

  • Vertical ordering

    Arrange the evolution of event handling optimization:

  • Code editor

    Create a CommunicationChannel component with useEffect that establishes a connection and cleans up on unmount.

  • Click in order

    Arrange the useEffect lifecycle:

  • Code editor

    Create an EarthCommunication component using useEffect to fetch data from an API and handle loading/error states.

  • Vertical ordering

    Arrange the useEffect mastery path:

  • Code editor

    Create ColonySettings with useEffect to sync state with localStorage (theme, language, etc.).

  • Code editor

    Create ShipDashboard with useState for fuel/temperature/engine, useEffect with a 2s interval, and buttons to control the systems

Useful articles