🔥 0
0
Lesson 6 of 9 20 min +100 XP

useEffect and Side Effects

While state and props handle data within React, many applications need to interact with the outside world - fetching data, setting up subscriptions, or manipulating the DOM directly. These are called side effects.

What are Side Effects?

Side effects are anything that affects something outside the scope of the current function:

  • Fetching data from an API
  • Subscriptions to external data sources
  • Timers (setTimeout, setInterval)
  • Logging to the console
  • Directly manipulating the DOM
  • Storing data in localStorage

The useEffect Hook

React provides useEffect to handle side effects:

import { useState, useEffect } from 'react';

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

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

    // Cleanup function
    return () => clearInterval(interval);
  }, []); // Empty array = run once

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

Dependency Array

The second argument controls when the effect runs:

No Dependency Array - Runs Every Render

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

Empty Array - Runs Once

useEffect(() => {
  console.log('Runs only after initial render');
}, []);

With Dependencies - Runs When Dependencies Change

useEffect(() => {
  console.log(`User ID changed to: ${userId}`);
  fetchUserData(userId);
}, [userId]); // Runs when userId changes

Fetching Data

A common use case for useEffect:

function UserProfile({ 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(`/api/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]); // Re-fetch when userId changes

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

  return <div>Hello, {user.name}!</div>;
}

Cleanup Function

The function returned from useEffect runs when:

  • The component unmounts
  • Before the effect re-runs (when dependencies change)
function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();

    // Cleanup: disconnect when leaving room
    return () => {
      connection.disconnect();
    };
  }, [roomId]);

  return <h1>Room: {roomId}</h1>;
}

Why Cleanup Matters

Without cleanup, you might:

  • Have memory leaks from subscriptions
  • Make unnecessary API calls
  • Have multiple intervals running
  • Listen to events multiple times
// ❌ Memory leak - no cleanup
useEffect(() => {
  window.addEventListener('resize', handleResize);
}, []);

// ✅ Proper cleanup
useEffect(() => {
  window.addEventListener('resize', handleResize);
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

Common Patterns

Document Title

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

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

Local Storage

function Settings() {
  const [theme, setTheme] = useState(() => {
    return localStorage.getItem('theme') || 'light';
  });

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

  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Toggle Theme ({theme})
    </button>
  );
}

Event Listeners

function ScrollTracker() {
  const [scrollY, setScrollY] = useState(0);

  useEffect(() => {
    const handleScroll = () => setScrollY(window.scrollY);

    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  return <p>Scroll position: {scrollY}px</p>;
}

Rules of useEffect

1. Don't Lie About Dependencies

// ❌ Missing dependency
useEffect(() => {
  fetchData(userId);
}, []); // userId should be in the array!

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

2. Keep Effects Focused

// ❌ Too many responsibilities
useEffect(() => {
  fetchUser();
  setupWebSocket();
  trackAnalytics();
}, []);

// ✅ Separate concerns
useEffect(() => { fetchUser(); }, []);
useEffect(() => { setupWebSocket(); }, []);
useEffect(() => { trackAnalytics(); }, []);

Key Takeaways

  • Side effects are operations outside React's render flow
  • useEffect runs after the component renders
  • The dependency array controls when the effect runs
  • Always clean up subscriptions, timers, and listeners
  • Don't lie about dependencies - include everything the effect uses

🧠 Quick Quiz

Test your understanding of this lesson.

1

What is a side effect in React?

2

When does useEffect run with an empty dependency array []?

3

What is the cleanup function in useEffect used for?

Event Handling