React useEffect Hook - Complete Guide

React useEffect Hook - Complete Guide

The useEffect hook is one of the most powerful and frequently used hooks in React. It lets you perform side effects in functional components, such as data fetching, subscriptions, timers, and manually changing the DOM. This guide covers everything you need to know about useEffect.

What is useEffect?

useEffect is a hook that lets you run side effects in your components. Side effects are operations that affect something outside the component scope, like:

  • Fetching data from APIs
  • Setting up subscriptions
  • Manually changing the DOM
  • Setting timers (setTimeout, setInterval)
  • Logging to the console

Basic useEffect Syntax

import { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // This code runs after the component renders
    console.log('Component rendered');
  });
  
  return <div>Hello World</div>;
}

useEffect with Dependencies

The dependency array controls when the effect runs:

1. No Dependencies (Runs on every render)

function EveryRenderEffect() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    console.log('Effect runs on every render');
    document.title = `Count: ${count}`;
  });
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

2. Empty Dependency Array (Runs once on mount)

function OnceEffect() {
  useEffect(() => {
    console.log('Component mounted');
    
    // This runs only once when the component mounts
    const timer = setTimeout(() => {
      console.log('Timer fired after 2 seconds');
    }, 2000);
    
    // Cleanup function
    return () => {
      clearTimeout(timer);
      console.log('Component unmounted');
    };
  }, []); // Empty dependency array
  
  return <div>Check the console</div>;
}

3. With Dependencies (Runs when dependencies change)

function DependencyEffect() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  useEffect(() => {
    console.log('Count changed:', count);
    document.title = `Count: ${count}`;
  }, [count]); // Only runs when count changes
  
  useEffect(() => {
    console.log('Name changed:', name);
  }, [name]); // Only runs when name changes
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter your name"
      />
    </div>
  );
}

Cleanup Functions

Effects can return cleanup functions that run when the component unmounts or before the effect re-runs:

function TimerComponent() {
  const [seconds, setSeconds] = useState(0);
  
  useEffect(() => {
    console.log('Setting up timer');
    
    const intervalId = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
    
    // Cleanup function
    return () => {
      console.log('Cleaning up timer');
      clearInterval(intervalId);
    };
  }, []); // Empty array means this effect runs only once
  
  return <div>Timer: {seconds} seconds</div>;
}

Common useEffect Patterns

1. Data Fetching

function DataFetcher({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`https://api.example.com/users/${userId}`);
        
        if (!response.ok) {
          throw new Error('Failed to fetch user');
        }
        
        const userData = await response.json();
        setUser(userData);
        setError(null);
      } catch (err) {
        setError(err.message);
        setUser(null);
      } finally {
        setLoading(false);
      }
    };
    
    if (userId) {
      fetchUser();
    }
  }, [userId]); // Re-fetch when userId changes
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user found</div>;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
    </div>
  );
}

2. Event Listeners

function WindowSizeTracker() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  
  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    window.addEventListener('resize', handleResize);
    
    // Cleanup
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Set up listener only once
  
  return (
    <div>
      <p>Window width: {windowSize.width}px</p>
      <p>Window height: {windowSize.height}px</p>
    </div>
  );
}

3. Local Storage

function PersistentCounter() {
  const [count, setCount] = useState(() => {
    // Initialize from localStorage
    const savedCount = localStorage.getItem('count');
    return savedCount ? parseInt(savedCount, 10) : 0;
  });
  
  useEffect(() => {
    // Save to localStorage whenever count changes
    localStorage.setItem('count', count.toString());
  }, [count]);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

4. WebSocket Connection

function WebSocketComponent({ url }) {
  const [messages, setMessages] = useState([]);
  const [connectionStatus, setConnectionStatus] = useState('disconnected');
  
  useEffect(() => {
    if (!url) return;
    
    const ws = new WebSocket(url);
    
    ws.onopen = () => {
      setConnectionStatus('connected');
      console.log('WebSocket connected');
    };
    
    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      setMessages(prev => [...prev, message]);
    };
    
    ws.onclose = () => {
      setConnectionStatus('disconnected');
      console.log('WebSocket disconnected');
    };
    
    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      setConnectionStatus('error');
    };
    
    // Cleanup
    return () => {
      if (ws.readyState === WebSocket.OPEN) {
        ws.close();
      }
    };
  }, [url]);
  
  return (
    <div>
      <p>Status: {connectionStatus}</p>
      <div>
        {messages.map((message, index) => (
          <div key={index}>{JSON.stringify(message)}</div>
        ))}
      </div>
    </div>
  );
}

Advanced useEffect Patterns

1. Custom Hook for API Calls

function useApi(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    let isCancelled = false;
    
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        
        const result = await response.json();
        
        if (!isCancelled) {
          setData(result);
          setError(null);
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err.message);
          setData(null);
        }
      } finally {
        if (!isCancelled) {
          setLoading(false);
        }
      }
    };
    
    if (url) {
      fetchData();
    }
    
    return () => {
      isCancelled = true;
    };
  }, [url]);
  
  return { data, loading, error };
}

// Usage
function UserProfile({ userId }) {
  const { data: user, loading, error } = useApi(
    userId ? `https://api.example.com/users/${userId}` : null
  );
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user data</div>;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

2. Debounced Search

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);
  
  return debouncedValue;
}

function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);
  const debouncedSearchTerm = useDebounce(searchTerm, 500);
  
  useEffect(() => {
    if (debouncedSearchTerm) {
      // Perform search with debounced term
      fetch(`https://api.example.com/search?q=${debouncedSearchTerm}`)
        .then(response => response.json())
        .then(data => setResults(data));
    } else {
      setResults([]);
    }
  }, [debouncedSearchTerm]);
  
  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />
      
      <div>
        {results.map((result, index) => (
          <div key={index}>{result.name}</div>
        ))}
      </div>
    </div>
  );
}

3. Previous Value Hook

function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value;
  });
  
  return ref.current;
}

function CounterWithPrevious() {
  const [count, setCount] = useState(0);
  const previousCount = usePrevious(count);
  
  return (
    <div>
      <p>Current count: {count}</p>
      <p>Previous count: {previousCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Common useEffect Mistakes and Solutions

1. Missing Dependencies

// Bad - missing dependencies
function BadExample({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => response.json())
      .then(data => setUser(data));
  }); // Missing userId dependency
  
  return <div>{user?.name}</div>;
}

// Good - include all dependencies
function GoodExample({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => response.json())
      .then(data => setUser(data));
  }, [userId]); // Include userId
  
  return <div>{user?.name}</div>;
}

2. Infinite Loops

// Bad - causes infinite loop
function BadLoop() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    setCount(count + 1); // This triggers the effect again
  }, [count]);
  
  return <div>Count: {count}</div>;
}

// Good - proper dependency management
function GoodLoop() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount(prev => prev + 1);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

3. Async Functions in useEffect

// Bad - async function directly in useEffect
function BadAsync() {
  const [data, setData] = useState(null);
  
  useEffect(async () => {
    const response = await fetch('https://api.example.com/data');
    const result = await response.json();
    setData(result);
  }, []);
  
  return <div>{JSON.stringify(data)}</div>;
}

// Good - async function inside useEffect
function GoodAsync() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/data');
        const result = await response.json();
        setData(result);
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };
    
    fetchData();
  }, []);
  
  return <div>{JSON.stringify(data)}</div>;
}

Performance Optimization

1. Memoizing Callbacks

function OptimizedComponent({ userId }) {
  const [user, setUser] = useState(null);
  
  // Memoize the callback to prevent unnecessary re-renders
  const handleUserUpdate = useCallback((updatedUser) => {
    setUser(updatedUser);
  }, []);
  
  useEffect(() => {
    // Effect will only re-run if userId or handleUserUpdate changes
    const fetchUser = async () => {
      const response = await fetch(`https://api.example.com/users/${userId}`);
      const userData = await response.json();
      handleUserUpdate(userData);
    };
    
    fetchUser();
  }, [userId, handleUserUpdate]);
  
  return <div>{user?.name}</div>;
}

2. Conditional Effects

function ConditionalEffect({ shouldFetch, userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    // Only run effect if conditions are met
    if (!shouldFetch || !userId) {
      return;
    }
    
    let isCancelled = false;
    
    const fetchUser = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        const userData = await response.json();
        
        if (!isCancelled) {
          setUser(userData);
        }
      } catch (error) {
        if (!isCancelled) {
          console.error('Error fetching user:', error);
        }
      }
    };
    
    fetchUser();
    
    return () => {
      isCancelled = true;
    };
  }, [shouldFetch, userId]);
  
  return <div>{user?.name}</div>;
}

useEffect vs useLayoutEffect

function EffectComparison() {
  const [content, setContent] = useState('Initial content');
  
  // useEffect - Runs after paint (non-blocking)
  useEffect(() => {
    console.log('useEffect ran');
    // Good for data fetching, subscriptions, etc.
  }, [content]);
  
  // useLayoutEffect - Runs before paint (blocking)
  useLayoutEffect(() => {
    console.log('useLayoutEffect ran');
    // Good for DOM measurements, layout calculations
    const height = document.getElementById('content').offsetHeight;
    console.log('Content height:', height);
  }, [content]);
  
  return (
    <div>
      <button onClick={() => setContent('Updated content')}>
        Update Content
      </button>
      <div id="content">{content}</div>
    </div>
  );
}

Summary

The useEffect hook is essential for handling side effects in React components. Remember these key points:

  • Use empty dependency array [] for effects that run once on mount
  • Include all variables used in the effect in the dependency array
  • Return cleanup functions to prevent memory leaks
  • Handle async operations properly inside effects
  • Use custom hooks to reuse effect logic
  • Be careful with dependencies to avoid infinite loops
  • Consider performance with useCallback and useMemo
  • Use useLayoutEffect for DOM measurements that need to happen before paint

Mastering useEffect will enable you to build sophisticated React applications that can handle real-world scenarios like data fetching, subscriptions, and DOM manipulation.


External Resources:

Related Tutorials:

Last updated on