useEffect Hook

Side effects, data fetching, and lifecycle management

What is useEffect?

useEffect is a React Hook that lets you perform side effects in your components. Side effects are anything that affects something outside the component: fetching data, updating the DOM, timers, subscriptions, and more.

What Are Side Effects?

  • • Fetching data from an API
  • • Setting up subscriptions (WebSocket, events)
  • • Manually changing the DOM
  • • Setting timers (setTimeout, setInterval)
  • • Logging
  • • Storing data in localStorage

Basic Syntax

import { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // This runs after every render
    console.log('Component rendered!');
  });

  return <div>Hello</div>;
}

The Dependency Array

The second argument controls when the effect runs:

// Runs after EVERY render
useEffect(() => {
  console.log('Runs every time');
});

// Runs only ONCE (on mount)
useEffect(() => {
  console.log('Runs once on mount');
}, []);  // Empty dependency array

// Runs when dependencies change
useEffect(() => {
  console.log('Count changed:', count);
}, [count]);  // Runs when count changes

// Multiple dependencies
useEffect(() => {
  console.log('User or page changed');
}, [userId, page]);  // Runs when either changes

Dependency Array Rules

Dependency Array When Effect Runs
undefined (no array) After every render
[] (empty array) Once on mount only
[value] On mount + when value changes
[a, b, c] On mount + when any dependency changes

Cleanup Function

Return a function to clean up when the component unmounts or before the effect runs again:

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // Set up the interval
    const intervalId = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // Cleanup function - runs on unmount
    return () => {
      clearInterval(intervalId);
      console.log('Timer cleaned up!');
    };
  }, []);  // Empty array = only run once

  return <p>Seconds: {seconds}</p>;
}

// Event listener example
function WindowSize() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    
    window.addEventListener('resize', handleResize);
    
    // Cleanup
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <p>Window width: {width}px</p>;
}

Data Fetching

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Reset state when userId changes
    setLoading(true);
    setError(null);

    fetch(`/api/users/${userId}`)
      .then(res => {
        if (!res.ok) throw new Error('Failed to fetch');
        return res.json();
      })
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, [userId]);  // Re-fetch when userId changes

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <h1>{user.name}</h1>;
}

Async/Await in useEffect

function DataFetcher() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // Can't make the effect function async directly
    // So create an async function inside
    const fetchData = async () => {
      try {
        const response = await fetch('/api/data');
        const json = await response.json();
        setData(json);
      } catch (error) {
        console.error('Error:', error);
      }
    };

    fetchData();
  }, []);

  return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

// With cleanup for race conditions
function SafeFetch({ id }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    let cancelled = false;

    const fetchData = async () => {
      const response = await fetch(`/api/items/${id}`);
      const json = await response.json();
      
      // Only update if not cancelled
      if (!cancelled) {
        setData(json);
      }
    };

    fetchData();

    // Cleanup: prevent state update if component unmounts
    return () => {
      cancelled = true;
    };
  }, [id]);

  return <div>{data?.name}</div>;
}

Common Use Cases

// Document title
function Page({ title }) {
  useEffect(() => {
    document.title = title;
  }, [title]);

  return <h1>{title}</h1>;
}

// Local storage sync
function Counter() {
  const [count, setCount] = useState(() => {
    return Number(localStorage.getItem('count')) || 0;
  });

  useEffect(() => {
    localStorage.setItem('count', count);
  }, [count]);

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// Focus on mount
function SearchInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} type="text" />;
}

Multiple Effects

function Dashboard({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);

  // Effect 1: Fetch user
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);

  // Effect 2: Fetch posts (separate concern)
  useEffect(() => {
    fetch(`/api/users/${userId}/posts`)
      .then(res => res.json())
      .then(setPosts);
  }, [userId]);

  // Effect 3: Document title (separate concern)
  useEffect(() => {
    if (user) {
      document.title = `${user.name}'s Dashboard`;
    }
  }, [user]);

  return <div>...</div>;
}

Common Mistakes

// ✗ Missing dependency
function BadExample({ userId }) {
  useEffect(() => {
    fetchUser(userId);  // userId used but not in deps!
  }, []);  // Should include userId

  // ✗ Object/array as dependency (causes infinite loop)
  const options = { page: 1 };  // New object every render!
  useEffect(() => {
    fetch('/api', options);
  }, [options]);  // Triggers every render!

  // ✓ Fix: Memoize or use primitives
  useEffect(() => {
    fetch('/api', { page: 1 });
  }, []);  // Or use useMemo for complex objects
}

💡 ESLint Plugin

Use eslint-plugin-react-hooks to catch missing dependencies automatically!

🎯 useEffect Best Practices

  • ✓ Always include all dependencies used inside the effect
  • ✓ Use cleanup functions for subscriptions, timers, event listeners
  • ✓ Keep effects focused—one effect per concern
  • ✓ Use empty [] for one-time setup on mount
  • ✓ Handle race conditions in async effects
  • ✓ Consider React Query or SWR for data fetching
  • ✓ Don't update state unconditionally in effects (infinite loop!)