JavaScript and React course Β· Module 5: useEffect and Custom Hooks
Performance Optimization with useEffect
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:
- Effects running too frequently - when dependencies change often or are improperly defined
- Expensive operations - when performing complex calculations or DOM manipulations inside effects
- Cascading updates - when one effect causes state changes that trigger subsequent effects
- 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:
useLayoutEffectblocks visual rendering, so it should be used sparingly and only when absolutely necessary.
Summary: Best Practices for Optimizing useEffect
- Minimize dependencies - use only necessary values in the dependency array
- Separate effects - instead of one complex effect, use several simpler ones
- Memoize dependencies - use useMemo and useCallback for objects and functions
- Use debouncing/throttling - for rapidly changing values
- Cancel outdated async operations - avoid race conditions and memory leaks
- Create custom hooks - encapsulate complex logic in reusable hooks
- Analyze rendering patterns - use React DevTools to detect excessive renders
- 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. What should the cleanup function in useEffect return?
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