โšก useEffect Hook

Managing Side Effects in React

What are Side Effects?

Side effects are operations that reach outside your component - like fetching data, updating the DOM, setting up subscriptions, or timers. useEffect lets you perform these operations at the right time.

Common Side Effects:

  • Fetching data from APIs
  • Subscribing to events
  • Setting up timers/intervals
  • Manually changing the DOM
  • Logging/analytics
  • Local storage operations

๐Ÿ“š Basic Syntax

import { useEffect } from 'react';

function Component() {
  useEffect(() => {
    // Side effect code here
    console.log('Effect ran!');
    
    // Optional: cleanup function
    return () => {
      console.log('Cleanup!');
    };
  }, [/* dependencies */]);
}

Three Forms of useEffect:

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

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

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

๐ŸŽฏ Dependency Array

The dependency array controls when your effect runs:

function Example() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  // Runs on every render (no dependency array)
  useEffect(() => {
    console.log('Every render');
  });
  
  // Runs once on mount (empty array)
  useEffect(() => {
    console.log('Component mounted');
  }, []);
  
  // Runs when count changes
  useEffect(() => {
    console.log('Count changed:', count);
  }, [count]);
  
  // Runs when either count OR name changes
  useEffect(() => {
    console.log('Count or name changed');
  }, [count, name]);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <input value={name} onChange={e => setName(e.target.value)} />
    </div>
  );
}

Rule: Include ALL Used Values

If your effect uses props, state, or functions, include them in the dependency array. ESLint will warn you if you forget!

๐Ÿงน Cleanup Functions

Return a cleanup function to prevent memory leaks:

// Timer cleanup
function Timer() {
  const [seconds, setSeconds] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
    
    // Cleanup: clear interval when component unmounts
    return () => clearInterval(interval);
  }, []);
  
  return <div>Seconds: {seconds}</div>;
}

// Event listener cleanup
function WindowSize() {
  const [width, setWidth] = useState(window.innerWidth);
  
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    
    window.addEventListener('resize', handleResize);
    
    // Cleanup: remove listener
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return <div>Width: {width}px</div>;
}

// Subscription cleanup
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  
  useEffect(() => {
    const subscription = subscribeToRoom(roomId, (message) => {
      setMessages(msgs => [...msgs, message]);
    });
    
    // Cleanup: unsubscribe
    return () => subscription.unsubscribe();
  }, [roomId]);
  
  return <div>{/* render messages */}</div>;
}

๐ŸŒ Fetching Data

Most common use case - fetch data from APIs:

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 user data
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => {
        if (!response.ok) throw new Error('Failed to fetch');
        return response.json();
      })
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, [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>{user.email}</p>
    </div>
  );
}

With Async/Await:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // Can't make useEffect callback async directly
    // Create async function inside
    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (error) {
        console.error(error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchUser();
  }, [userId]);
  
  // ... rest of component
}

๐Ÿ”„ Cleanup for Fetch Requests

Prevent race conditions and memory leaks:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    // Flag to track if component is still mounted
    let cancelled = false;
    
    const searchAPI = async () => {
      setLoading(true);
      
      try {
        const response = await fetch(`/api/search?q=${query}`);
        const data = await response.json();
        
        // Only update state if component is still mounted
        if (!cancelled) {
          setResults(data);
          setLoading(false);
        }
      } catch (error) {
        if (!cancelled) {
          console.error(error);
          setLoading(false);
        }
      }
    };
    
    searchAPI();
    
    // Cleanup: mark as cancelled
    return () => {
      cancelled = true;
    };
  }, [query]);
  
  return (
    <div>
      {loading && <p>Searching...</p>}
      {results.map(result => <div key={result.id}>{result.title}</div>)}
    </div>
  );
}

Using AbortController (Modern Approach):

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    const controller = new AbortController();
    
    fetch(`/api/search?q=${query}`, {
      signal: controller.signal
    })
      .then(res => res.json())
      .then(data => setResults(data))
      .catch(error => {
        if (error.name !== 'AbortError') {
          console.error(error);
        }
      });
    
    // Cleanup: abort request
    return () => controller.abort();
  }, [query]);
  
  return <div>{/* render results */}</div>;
}

๐Ÿ’พ Syncing with localStorage

function usePersistentState(key, initialValue) {
  // Load from localStorage
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved !== null ? JSON.parse(saved) : initialValue;
  });
  
  // Save to localStorage whenever value changes
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  
  return [value, setValue];
}

// Usage
function TodoApp() {
  const [todos, setTodos] = usePersistentState('todos', []);
  
  // Todos automatically saved to localStorage!
  const addTodo = (text) => {
    setTodos([...todos, { id: Date.now(), text, done: false }]);
  };
  
  return <div>{/* render todos */}</div>;
}

๐ŸŽฏ Common Patterns

1. Document Title:

function PageTitle({ title }) {
  useEffect(() => {
    document.title = title;
  }, [title]);
  
  return <div>{/* page content */}</div>;
}

// Custom hook version
function useDocumentTitle(title) {
  useEffect(() => {
    const prevTitle = document.title;
    document.title = title;
    
    // Restore previous title on unmount
    return () => {
      document.title = prevTitle;
    };
  }, [title]);
}

// Usage
function HomePage() {
  useDocumentTitle('Home - My App');
  return <div>Home Page</div>;
}

2. Debounced Search:

function SearchBox() {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    // Don't search for empty term
    if (!searchTerm) {
      setResults([]);
      return;
    }
    
    // Delay search by 500ms
    const timeoutId = setTimeout(() => {
      fetch(`/api/search?q=${searchTerm}`)
        .then(res => res.json())
        .then(setResults);
    }, 500);
    
    // Cleanup: cancel previous timeout
    return () => clearTimeout(timeoutId);
  }, [searchTerm]);
  
  return (
    <div>
      <input 
        value={searchTerm}
        onChange={e => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />
      {results.map(r => <div key={r.id}>{r.name}</div>)}
    </div>
  );
}

3. WebSocket Connection:

function LiveFeed() {
  const [messages, setMessages] = useState([]);
  
  useEffect(() => {
    const ws = new WebSocket('wss://api.example.com/live');
    
    ws.onopen = () => {
      console.log('Connected');
    };
    
    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      setMessages(prev => [...prev, message]);
    };
    
    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };
    
    // Cleanup: close connection
    return () => {
      ws.close();
    };
  }, []);  // Empty array = connect once on mount
  
  return (
    <div>
      {messages.map((msg, i) => (
        <div key={i}>{msg.text}</div>
      ))}
    </div>
  );
}

๐ŸŽฏ Complete Example: Weather App

function WeatherApp() {
  const [city, setCity] = useState('London');
  const [weather, setWeather] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [lastUpdated, setLastUpdated] = useState(null);
  
  // Fetch weather data
  useEffect(() => {
    const controller = new AbortController();
    
    const fetchWeather = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const response = await fetch(
          `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=YOUR_API_KEY`,
          { signal: controller.signal }
        );
        
        if (!response.ok) {
          throw new Error('City not found');
        }
        
        const data = await response.json();
        setWeather(data);
        setLastUpdated(new Date());
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    };
    
    fetchWeather();
    
    return () => controller.abort();
  }, [city]);
  
  // Auto-refresh every 5 minutes
  useEffect(() => {
    const interval = setInterval(() => {
      setCity(c => c);  // Trigger re-fetch
    }, 5 * 60 * 1000);
    
    return () => clearInterval(interval);
  }, []);
  
  // Update document title
  useEffect(() => {
    if (weather) {
      document.title = `${weather.main.temp}ยฐC - ${city}`;
    }
  }, [weather, city]);
  
  return (
    <div className="weather-app">
      <input 
        value={city}
        onChange={e => setCity(e.target.value)}
        placeholder="Enter city"
      />
      
      {loading && <div>Loading weather...</div>}
      
      {error && (
        <div className="error">
          Error: {error}
        </div>
      )}
      
      {weather && !loading && (
        <div className="weather-info">
          <h2>{weather.name}</h2>
          <p>Temperature: {Math.round(weather.main.temp - 273.15)}ยฐC</p>
          <p>Condition: {weather.weather[0].description}</p>
          <p>Humidity: {weather.main.humidity}%</p>
          {lastUpdated && (
            <small>Last updated: {lastUpdated.toLocaleTimeString()}</small>
          )}
        </div>
      )}
    </div>
  );
}

โš ๏ธ Common Mistakes

1. Missing Dependencies

// โŒ Wrong - missing 'count' in dependencies
useEffect(() => {
  console.log(count);
}, []);

// โœ… Correct
useEffect(() => {
  console.log(count);
}, [count]);

2. Infinite Loops

// โŒ Wrong - creates infinite loop
useEffect(() => {
  setCount(count + 1);
}, [count]);  // count changes โ†’ effect runs โ†’ count changes...

// โœ… Use different trigger or remove dependency
useEffect(() => {
  // Run once on mount
  setCount(c => c + 1);
}, []);

3. Async useEffect Callback

// โŒ Wrong - can't make callback async
useEffect(async () => {
  const data = await fetchData();
}, []);

// โœ… Correct - async function inside
useEffect(() => {
  const fetchData = async () => {
    const data = await fetch('/api');
  };
  fetchData();
}, []);

๐ŸŽฏ Key Takeaways