Skip to main content

Command Palette

Search for a command to run...

useEffect

Published
•7 min read•View as Markdown
useEffect

Understanding useEffect: React's "Do Something After Render" Hook

Hey! Now let's learn about useEffect - it's like giving your component superpowers to do things at specific times.

What Does useEffect Do?

Think of useEffect as a side effect manager. It lets your component:

  • Fetch data when it first loads

  • Update something when data changes

  • Clean up when the component disappears

  • Listen to events (like window resizing)

The Basic Pattern

useEffect(() => {
  // Code that runs after render
}, [dependencies]); // Optional dependency array

Real-Life Example: A Page Title Updater

Imagine you want to change your browser tab title whenever something changes in your app:

import React, { useState, useEffect } from 'react';

function PageTitleUpdater() {
  const [pageName, setPageName] = useState('Home');

  // EFFECT 1: Run once when component loads
  useEffect(() => {
    console.log('Component loaded!');
    document.title = `My App - ${pageName}`;
  }, []); // Empty array = run once

  // EFFECT 2: Run whenever pageName changes
  useEffect(() => {
    console.log('Page name changed to:', pageName);
    document.title = `My App - ${pageName}`;
  }, [pageName]); // Run when pageName changes

  return (
    <div>
      <h1>Welcome to {pageName} Page</h1>
      <button onClick={() => setPageName('Home')}>Home</button>
      <button onClick={() => setPageName('About')}>About</button>
      <button onClick={() => setPageName('Contact')}>Contact</button>
    </div>
  );
}

Three Main Ways to Use useEffect

1. Run Once (On Mount)

useEffect(() => {
  // This runs ONCE when component first appears
  console.log('Component loaded for the first time');

  // Common uses:
  // - Fetch initial data
  // - Set up timers
  // - Connect to WebSocket
}, []); // ← Empty brackets mean "run once"

Example: Fetch data when page loads

function UserProfile() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // Fetch user data when component loads
    fetch('https://api.example.com/user')
      .then(response => response.json())
      .then(data => setUser(data));

    console.log('Data fetch started');
  }, []); // Runs once

  return (
    <div>
      {user ? (
        <p>Welcome, {user.name}!</p>
      ) : (
        <p>Loading user data...</p>
      )}
    </div>
  );
}

2. Run When Something Changes

useEffect(() => {
  // This runs whenever 'count' changes
  console.log(`Count changed to: ${count}`);
}, [count]); // ← Put variables to watch here

Example: Save form data when it changes

function AutoSaveForm() {
  const [formData, setFormData] = useState({
    title: '',
    content: ''
  });

  // Save whenever formData changes
  useEffect(() => {
    console.log('Saving form data:', formData);

    // In real app, you might:
    // 1. Save to localStorage
    localStorage.setItem('draft', JSON.stringify(formData));

    // 2. Or send to API
    // api.saveDraft(formData);

  }, [formData]); // Runs when formData changes

  return (
    <div>
      <input
        placeholder="Title"
        value={formData.title}
        onChange={(e) => setFormData({...formData, title: e.target.value})}
      />
      <textarea
        placeholder="Content"
        value={formData.content}
        onChange={(e) => setFormData({...formData, content: e.target.value})}
      />
    </div>
  );
}

3. Cleanup (Prevent Memory Leaks)

useEffect(() => {
  // Setup code here
  const interval = setInterval(() => {
    console.log('Tick');
  }, 1000);

  // Cleanup function (runs before next effect OR when component unmounts)
  return () => {
    console.log('Cleaning up...');
    clearInterval(interval);
  };
}, []);

Example: Live Clock with Cleanup

function LiveClock() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    console.log('Clock started');

    // Start timer
    const timerId = setInterval(() => {
      setTime(new Date());
    }, 1000);

    // Cleanup function
    return () => {
      console.log('Clock stopped');
      clearInterval(timerId);
    };
  }, []); // Empty array = run once and clean up once

  return (
    <div>
      <h2>Current Time: {time.toLocaleTimeString()}</h2>
    </div>
  );
}

Common Real-World Examples

Example 1: Window Event Listener

function WindowSizeTracker() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    console.log('Setting up resize listener');

    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    // Add event listener
    window.addEventListener('resize', handleResize);

    // Cleanup: Remove event listener
    return () => {
      console.log('Removing resize listener');
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Run once on mount

  return (
    <div>
      <p>Window Size: {windowSize.width} x {windowSize.height}</p>
      <p>{windowSize.width < 768 ? 'Mobile' : 'Desktop'} view</p>
    </div>
  );
}

Example 2: API Data Fetching with Loading States

function WeatherApp() {
  const [city, setCity] = useState('London');
  const [weather, setWeather] = useState(null);
  const [loading, setLoading] = useState(false);

  // Fetch weather whenever city changes
  useEffect(() => {
    let isMounted = true; // Prevent state updates if component unmounts

    const fetchWeather = async () => {
      setLoading(true);
      try {
        const response = await fetch(
          `https://api.weather.com/${city}`
        );
        const data = await response.json();

        // Only update state if component is still mounted
        if (isMounted) {
          setWeather(data);
        }
      } catch (error) {
        if (isMounted) {
          console.error('Failed to fetch weather:', error);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchWeather();

    // Cleanup function
    return () => {
      isMounted = false;
    };
  }, [city]); // Re-fetch when city changes

  return (
    <div>
      <input
        value={city}
        onChange={(e) => setCity(e.target.value)}
        placeholder="Enter city"
      />

      {loading ? (
        <p>Loading weather for {city}...</p>
      ) : weather ? (
        <div>
          <h3>Weather in {city}</h3>
          <p>Temperature: {weather.temp}°C</p>
          <p>Condition: {weather.condition}</p>
        </div>
      ) : (
        <p>Enter a city to see weather</p>
      )}
    </div>
  );
}

Example 3: Simple Chat Notification

function ChatApp() {
  const [messages, setMessages] = useState([]);
  const [unreadCount, setUnreadCount] = useState(0);

  // When messages change, update unread count
  useEffect(() => {
    setUnreadCount(messages.length);

    // Show notification for new messages
    if (messages.length > 0) {
      const lastMessage = messages[messages.length - 1];
      console.log(`New message: ${lastMessage.text}`);

      // Browser notification (if permitted)
      if (Notification.permission === 'granted') {
        new Notification('New Message', {
          body: lastMessage.text
        });
      }
    }
  }, [messages]); // Run when messages array changes

  const addMessage = (text) => {
    setMessages([...messages, { text, time: new Date() }]);
  };

  return (
    <div>
      <h2>Chat ({unreadCount} messages)</h2>
      <button onClick={() => addMessage('Hello!')}>
        Add Test Message
      </button>
      <button onClick={() => addMessage('How are you?')}>
        Add Another
      </button>

      <div>
        {messages.map((msg, index) => (
          <p key={index}>{msg.text}</p>
        ))}
      </div>
    </div>
  );
}

The Dependency Array: When Effects Run

// 1. No array = runs after EVERY render (careful!)
useEffect(() => {
  console.log('Runs after every single render');
});

// 2. Empty array = runs ONCE after first render
useEffect(() => {
  console.log('Runs once when component mounts');
}, []);

// 3. With dependencies = runs when those values change
useEffect(() => {
  console.log('Runs when count OR text changes');
}, [count, text]);

Common Mistakes to Avoid

Mistake 1: Missing dependencies

const [count, setCount] = useState(0);
const [result, setResult] = useState(0);

// ❌ WRONG: Forgot count in dependencies
useEffect(() => {
  setResult(count * 2);
}, []); // This won't update when count changes!

// ✅ CORRECT: Include all used variables
useEffect(() => {
  setResult(count * 2);
}, [count]); // Now it updates when count changes

Mistake 2: Infinite loops

const [count, setCount] = useState(0);

// ❌ INFINITE LOOP: Updates state, triggers effect, updates state...
useEffect(() => {
  setCount(count + 1); // Bad!
}, [count]);

// ✅ Solution: Add condition or fix logic
useEffect(() => {
  if (count < 10) {
    setCount(count + 1);
  }
}, [count]);

Mistake 3: Forgetting cleanup

// ❌ MEMORY LEAK: Event listener not removed
useEffect(() => {
  window.addEventListener('scroll', handleScroll);
}, []);

// ✅ CORRECT: Always clean up
useEffect(() => {
  window.addEventListener('scroll', handleScroll);

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

Practice Exercise: Build a Stopwatch

Try creating a stopwatch that:

  1. Starts counting when you click "Start"

  2. Stops when you click "Stop"

  3. Resets when you click "Reset"

  4. Shows the elapsed time

function Stopwatch() {
  const [time, setTime] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  // Your useEffect goes here!
  // Hint: You'll need setInterval and cleanup

  return (
    <div>
      <h1>{time} seconds</h1>
      <button onClick={() => setIsRunning(true)}>Start</button>
      <button onClick={() => setIsRunning(false)}>Stop</button>
      <button onClick={() => setTime(0)}>Reset</button>
    </div>
  );
}

When to Use useEffect?

✅ DO use useEffect for:

  • Fetching data on component load

  • Setting up subscriptions (WebSocket, event listeners)

  • Updating DOM in response to state changes

  • Cleaning up resources

❌ DON'T use useEffect for:

  • Event handlers (use onClick instead)

  • Derived state (calculate directly in render)

  • Synchronizing state that should update together

Summary Cheat Sheet

// 1. Run once on mount (like componentDidMount)
useEffect(() => {
  // Initial setup here
}, []);

// 2. Run when dependencies change (like componentDidUpdate)
useEffect(() => {
  // Update logic here
}, [dep1, dep2]);

// 3. Cleanup (like componentWillUnmount)
useEffect(() => {
  // Setup
  return () => {
    // Cleanup
  };
}, []);

// 4. Run on every render (use sparingly!)
useEffect(() => {
  // Runs after every render
});

Remember: useEffect is your tool for "side effects" - anything that happens after render. Start simple, add dependencies carefully, and always clean up!