useEffect runs side effects after render — API calls, subscriptions, timers, and DOM manipulation.

Basic Syntax

  import { useState, useEffect } from 'react';

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

    useEffect(() => {
        const interval = setInterval(() => {
            setSeconds(s => s + 1);
        }, 1000);

        return () => clearInterval(interval); // Cleanup
    }, []); // Empty deps = run once on mount

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

Dependency Array

  // Run on every render
useEffect(() => { /* ... */ });

// Run once on mount
useEffect(() => { /* ... */ }, []);

// Run when count changes
useEffect(() => {
    document.title = `Count: ${count}`;
}, [count]);
  

Data Fetching

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

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

        async function fetchUser() {
            try {
                setLoading(true);
                const res = await fetch(`/api/users/${userId}`);
                if (!res.ok) throw new Error('Failed to fetch');
                const data = await res.json();
                if (!cancelled) setUser(data);
            } catch (err) {
                if (!cancelled) setError(err.message);
            } finally {
                if (!cancelled) setLoading(false);
            }
        }

        fetchUser();
        return () => { cancelled = true; }; // Cancel on unmount/re-fetch
    }, [userId]);

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

Cleanup Function

Return a function from useEffect to clean up:

  useEffect(() => {
    function handleResize() {
        console.log('Window resized');
    }
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
}, []);
  

Multiple Effects

Separate concerns with multiple useEffect calls:

  function Dashboard({ userId }) {
    // Fetch user
    useEffect(() => {
        fetchUser(userId);
    }, [userId]);

    // Fetch notifications
    useEffect(() => {
        fetchNotifications(userId);
    }, [userId]);

    // Analytics
    useEffect(() => {
        trackPageView('dashboard');
    }, []);
}
  

Common Patterns

Sync with localStorage

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

Focus input on mount

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

Avoid Common Mistakes

  // WRONG — infinite loop
useEffect(() => {
    setCount(count + 1);
}); // No dependency array

// WRONG — missing dependency
useEffect(() => {
    fetchData(userId);
}, []); // Should include userId

// CORRECT
useEffect(() => {
    fetchData(userId);
}, [userId]);
  

For complex data fetching, consider TanStack Query (React Query) which handles caching, refetching, and loading states automatically.