Featured image of post React useEffect Tutorial: From Basic to Advanced

React useEffect Tutorial: From Basic to Advanced

Master React's useEffect hook with practical examples. Learn to control component lifecycles and implement cleanup functions for better React apps.

Have you ever needed to run code right after your React component renders on screen?

That’s exactly what useEffect is for. Whether you need to fetch data, set up event listeners, or update the DOM, useEffect helps you perform these actions at the right time. This guide breaks down this essential React hook with practical examples you can start using today.



What is useEffect?

Think of useEffect as React’s way to handle “side effects” – anything that happens outside your component’s rendering process. It lets you run code after React has updated the DOM.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import React, { useEffect } from 'react';

function WelcomeMessage() {
  useEffect(() => {
    // This runs after component renders
    console.log('Welcome message is now visible!');
    
    // This runs when component is removed
    return () => {
      console.log('Welcome message is being removed');
    };
  }, []); // The empty array means "run once after first render"
  
  return <h2>Welcome to our app!</h2>;
}
  • How It Works
    useEffect takes two arguments: a function to run and a dependency array.
    The empty array [] tells React to run your effect only once when the component mounts.

  • Real-World Analogy
    It’s like a smart home system: when you enter a room (component renders), the lights turn on automatically (useEffect runs). When you leave (component unmounts), the lights turn off (cleanup function runs).


Controlling When Effects Run

One of useEffect’s most powerful features is the ability to control exactly when your effects run:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function UserProfile() {
  const [userId, setUserId] = useState(1);
  const [darkMode, setDarkMode] = useState(false);
  
  // Runs only once when the component first appears
  useEffect(() => {
    document.title = 'User Profile Page';
  }, []);
  
  // Runs whenever userId changes
  useEffect(() => {
    console.log(`Fetching data for user #${userId}`);
  }, [userId]);
  
  // Runs after every render
  useEffect(() => {
    console.log(`Mode: ${darkMode ? 'Dark' : 'Light'}, User: #${userId}`);
  });
  
  return (
    <div className={darkMode ? 'dark' : 'light'}>
      <h1>User Profile: #{userId}</h1>
      <button onClick={() => setUserId(userId + 1)}>Next User</button>
      <button onClick={() => setDarkMode(!darkMode)}>
        Toggle {darkMode ? 'Light' : 'Dark'} Mode
      </button>
    </div>
  );
}
  • Empty Array ([])
    Your effect runs exactly once after the first render – perfect for initialization tasks like setting page titles or initial data fetching.

  • With Dependencies ([userId])
    Your effect runs whenever any value in the dependency array changes – great for keeping operations in sync with specific state changes.

  • No Dependency Array
    Your effect runs after every render – useful in rare cases but can cause performance issues if overused.



Cleaning Up Effects

Proper cleanup is crucial for preventing memory leaks and unexpected behavior:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function WindowSizeTracker() {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);
  
  useEffect(() => {
    // Define handler
    function handleResize() {
      setWindowWidth(window.innerWidth);
    }
    
    // Set up listener
    window.addEventListener('resize', handleResize);
    
    // Return cleanup function
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);
  
  return (
    <div>
      <p>Current window width: {windowWidth}px</p>
      <p>{windowWidth < 768 ? 'Mobile view' : 'Desktop view'}</p>
    </div>
  );
}
  • When Cleanup Happens
    Cleanup functions run before the component unmounts or before the effect runs again due to dependency changes – ensuring resources are properly released.

  • What Requires Cleanup
    Always clean up event listeners, timers (setTimeout/setInterval), subscriptions, WebSockets, and other ongoing processes to prevent memory leaks.


Handling API Requests with useEffect

Data fetching is perhaps the most common useEffect use case:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function WeatherDisplay() {
  const [weather, setWeather] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // Track if component is still mounted
    let isMounted = true;
    
    async function fetchWeather() {
      try {
        setLoading(true);
        const response = await fetch('https://api.example.com/weather/current');
        
        if (!response.ok) {
          throw new Error(`API error: ${response.status}`);
        }
        
        const data = await response.json();
        
        // Only update state if component is still mounted
        if (isMounted) {
          setWeather(data);
          setError(null);
        }
      } catch (err) {
        if (isMounted) {
          setError(`Failed to load weather: ${err.message}`);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    }
    
    fetchWeather();
    
    // Cleanup: prevent state updates after unmount
    return () => {
      isMounted = false;
    };
  }, []);
  
  if (loading) return <div>Loading weather data...</div>;
  if (error) return <div>{error}</div>;
  if (!weather) return null;
  
  return (
    <div>
      <h1>Current Weather</h1>
      <div>Temperature: {weather.temperature}°F</div>
      <div>Condition: {weather.condition}</div>
    </div>
  );
}
  • Best Practices
    Use an “isMounted” flag to prevent state updates after component unmounting – avoiding React warnings and memory leaks.
    Always include loading and error states to create a better user experience.

Managing Component Lifecycles

useEffect is perfect for handling different stages of your component’s lifecycle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function Modal({ isOpen, onClose, message }) {
  // "When component mounts" effect
  useEffect(() => {
    console.log('Modal component created');
    return () => console.log('Modal component destroyed');
  }, []);
  
  // "When prop changes" effect
  useEffect(() => {
    if (isOpen) {
      // Disable scrolling when modal opens
      document.body.style.overflow = 'hidden';
    }
    
    return () => {
      if (isOpen) {
        // Re-enable scrolling when modal closes
        document.body.style.overflow = '';
      }
    };
  }, [isOpen]);
  
  if (!isOpen) return null;
  
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        <button onClick={onClose}>Close</button>
        <div>{message}</div>
      </div>
    </div>
  );
}
  • Understanding Lifecycles
    “Mounting” is when a component first appears on screen, and “unmounting” is when it’s removed.
    With useEffect, you can target specific lifecycle events by using appropriate dependency arrays.


Common useEffect Pitfalls and Solutions

Avoiding Infinite Loops

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ❌ Problem: Creates an infinite loop
function BadCounter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    setCount(count + 1); // Updates state, triggering re-render and another effect run
  }, [count]);
  
  return <div>{count}</div>;
}

// ✅ Solution: Use functional updates
function GoodCounter() {
  const [count, setCount] = useState(0);
  const [shouldCount, setShouldCount] = useState(false);
  
  useEffect(() => {
    if (shouldCount) {
      // This approach doesn't need count in dependencies
      setCount(prev => (prev < 5 ? prev + 1 : prev));
    }
  }, [shouldCount]);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setShouldCount(!shouldCount)}>
        {shouldCount ? 'Stop' : 'Start'} Counting
      </button>
    </div>
  );
}

Including All Dependencies

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// ✅ Properly handling dependencies
function SearchComponent({ searchTerm }) {
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    // Skip empty searches
    if (!searchTerm.trim()) return;
    
    // Debounce searches while typing
    const timer = setTimeout(() => {
      fetch(`https://api.example.com/search?q=${searchTerm}`)
        .then(r => r.json())
        .then(setResults);
    }, 300);
    
    // Clean up timer if searchTerm changes before timeout
    return () => clearTimeout(timer);
  }, [searchTerm]); // searchTerm correctly included in dependencies
  
  return <ResultsList items={results} />;
}

Separating Effects by Concern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// ✅ Separate effects for separate concerns
function ProfilePage({ userId }) {
  // Data fetching effect
  useEffect(() => {
    fetchUserData(userId);
  }, [userId]);
  
  // DOM event handling effect
  useEffect(() => {
    function handleResize() {/* resize logic */}
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  // Analytics effect
  useEffect(() => {
    logPageView('profile');
  }, []);
}

Key Takeaways

useEffect is a powerful tool for synchronizing your React components with external systems. Remember these core principles:

  • Use dependency arrays strategically to control when your effects run
  • Always clean up resources you create in your effects
  • Keep each effect focused on a single responsibility
  • Be careful with state updates in effects to avoid infinite loops

Mastering useEffect will help you build more responsive, efficient React applications that handle complex interactions smoothly while maintaining great performance.

Have a question about useEffect or a tricky use case? Drop a comment below!


Hugo로 만듦
JimmyStack 테마 사용 중