Code To Learn logo

Code To Learn

M3: Effects & Data

L1: Introduction to useEffect

Learn how to handle side effects in React components using the useEffect hook

What You'll Learn

In this lesson, you'll discover how React handles side effects - operations that reach outside your component to interact with the browser, APIs, or external systems. You'll learn:

  • What side effects are and why they matter
  • How to use the useEffect hook
  • Understanding the dependency array
  • Common patterns and best practices
  • When to use useEffect vs when not to

Understanding Side Effects

What is a Side Effect?

In React, a side effect is any operation that affects something outside the scope of the current component being rendered. Think of it as reaching beyond your component's borders.

Examples of side effects:

  • 🌐 Fetching data from an API
  • 📝 Updating the document title
  • ⏰ Setting up timers or intervals
  • 🔔 Subscribing to external data sources
  • 💾 Reading/writing to localStorage
  • 📊 Logging analytics events
  • 🎨 Manually manipulating the DOM

Key Concept: Side effects happen outside the normal React render flow. They're operations that can't be done during rendering.

Why Do We Need useEffect?

React components should be pure functions during rendering - they take props and state, and return JSX. But real applications need to do more:

❌ Problem: Side effects during render
function Component() {
  // This runs during EVERY render!
  document.title = "My App"; // Side effect!
  fetch("/api/listings"); // Side effect!
  
  return <div>Component</div>;
}

This causes problems:

  • 🔄 Side effects run on every render (too often!)
  • 🐛 No way to clean up (memory leaks)
  • ⚡ Can't control when they run
  • 🔁 Can cause infinite loops

useEffect solves this:

✅ Solution: useEffect controls when side effects run
import { useEffect } from 'react';

function Component() {
  useEffect(() => {
    document.title = "My App";
  }, []); // Runs only once!
  
  return <div>Component</div>;
}

The useEffect Hook

Basic Syntax

useEffect Basic Pattern
import { useEffect } from 'react';

function Component() {
  useEffect(() => {
    // Your side effect code here
    console.log('Effect ran!');
  }, [dependencies]);
  
  return <div>Component</div>;
}

The three parts:

  1. Effect function - The code you want to run
  2. Dependencies array - Controls when the effect runs
  3. Cleanup function (optional) - Runs before the effect re-runs or component unmounts

When Does useEffect Run?

The timing depends on the dependency array:

Runs Once After Mount
useEffect(() => {
  console.log('Component mounted!');
}, []); // Empty array = run once

When: Once, after the first render

Use case: Initial data fetching, subscriptions, one-time setup

Runs When Dependencies Change
useEffect(() => {
  console.log(`Count changed to ${count}`);
}, [count]); // Runs when count changes

When: After first render, and whenever dependencies change

Use case: Syncing with external systems, responding to state changes

Runs After Every Render
useEffect(() => {
  console.log('Component rendered!');
}); // No array = run every render

When: After every render (usually not what you want!)

Use case: Rare - usually indicates a code smell

Important: useEffect runs after the component renders and the browser paints the screen. This prevents blocking the UI.

Common Patterns

Pattern 1: Document Title

Update the browser tab title based on component state:

src/pages/HomePage.jsx
import { useState, useEffect } from 'react';

function HomePage() {
  const [listings, setListings] = useState([]);

  useEffect(() => {
    document.title = `StaySense - ${listings.length} listings`;
  }, [listings]); // Updates when listings change

  return (
    <div>
      <h1>Available Listings: {listings.length}</h1>
      {/* ... */}
    </div>
  );
}

Why it works:

  • Effect runs when listings changes
  • Document title stays in sync with state
  • Clean and declarative

Pattern 2: Console Logging (Debugging)

Track when your component renders and why:

Debugging Component Lifecycle
import { useEffect } from 'react';

function PropertyCard({ listing }) {
  useEffect(() => {
    console.log('PropertyCard mounted:', listing.id);
    
    return () => {
      console.log('PropertyCard unmounting:', listing.id);
    };
  }, [listing.id]);

  useEffect(() => {
    console.log('Listing data changed:', listing);
  }, [listing]);

  return <div>{listing.title}</div>;
}

What happens:

  • First effect logs when component mounts/unmounts
  • Second effect logs when listing data changes
  • Cleanup function runs before unmount

Pattern 3: Timer/Interval

Set up timers that automatically clean up:

Auto-updating Timestamp
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const interval = setInterval(() => {
      setTime(new Date());
    }, 1000);

    // Cleanup: Clear interval when component unmounts
    return () => clearInterval(interval);
  }, []); // Empty array = set up once

  return <div>{time.toLocaleTimeString()}</div>;
}

Key points:

  • setInterval is a side effect (it runs a background timer)
  • Cleanup function prevents memory leaks
  • Empty dependency array means timer is set up once

Pattern 4: Event Listeners

Add and remove event listeners properly:

Window Resize Listener
import { useState, useEffect } from 'react';

function ResponsiveComponent() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => {
      setWidth(window.innerWidth);
    };

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

    // Cleanup: Remove listener
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Set up once

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

Rules of useEffect

Comparison: useEffect vs Class Lifecycle

If you're coming from class components, here's how useEffect relates:

Class Component
class Component extends React.Component {
  componentDidMount() {
    fetchData();
  }
}
Function Component with useEffect
function Component() {
  useEffect(() => {
    fetchData();
  }, []); // Empty array = mount only
}
Class Component
class Component extends React.Component {
  componentDidUpdate(prevProps) {
    if (prevProps.userId !== this.props.userId) {
      fetchUser(this.props.userId);
    }
  }
}
Function Component with useEffect
function Component({ userId }) {
  useEffect(() => {
    fetchUser(userId);
  }, [userId]); // Runs when userId changes
}
Class Component
class Component extends React.Component {
  componentWillUnmount() {
    subscription.unsubscribe();
  }
}
Function Component with useEffect
function Component() {
  useEffect(() => {
    return () => {
      subscription.unsubscribe();
    };
  }, []);
}
Class Component
class Component extends React.Component {
  componentDidMount() {
    this.subscribe();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.id !== this.props.id) {
      this.unsubscribe();
      this.subscribe();
    }
  }

  componentWillUnmount() {
    this.unsubscribe();
  }

  subscribe() { /* ... */ }
  unsubscribe() { /* ... */ }
}
Function Component with useEffect
function Component({ id }) {
  useEffect(() => {
    subscribe(id);
    
    return () => unsubscribe(id);
  }, [id]); // Handles all three lifecycle methods!
}

Much simpler! One useEffect handles mount, update, and unmount.

When NOT to Use useEffect

Some things don't need useEffect:

❌ Don't: Calculate Derived State

❌ Wrong: useEffect for derived state
function Component({ items }) {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(items.length); // Unnecessary effect!
  }, [items]);

  return <div>{count} items</div>;
}
✅ Correct: Calculate during render
function Component({ items }) {
  const count = items.length; // Just calculate it!

  return <div>{count} items</div>;
}

❌ Don't: Handle User Events

❌ Wrong: useEffect for event handling
function Component() {
  const [clicked, setClicked] = useState(false);

  useEffect(() => {
    if (clicked) {
      alert('Clicked!');
      setClicked(false);
    }
  }, [clicked]);

  return <button onClick={() => setClicked(true)}>Click</button>;
}
✅ Correct: Direct event handler
function Component() {
  const handleClick = () => {
    alert('Clicked!');
  };

  return <button onClick={handleClick}>Click</button>;
}

Use event handlers for user interactions, not useEffect!

❌ Don't: Initialize State

❌ Wrong: useEffect for initial state
function Component({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    setUser({ id: userId, name: 'Unknown' });
  }, []);

  return <div>{user?.name}</div>;
}
✅ Correct: Initialize in useState
function Component({ userId }) {
  const [user, setUser] = useState({ 
    id: userId, 
    name: 'Unknown' 
  });

  return <div>{user.name}</div>;
}

Practice Exercise

Let's practice what you've learned! Try to predict what happens:

Exercise 1: Predict the Output

Exercise
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('Effect ran, count:', count);
  }, [count]);

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

Questions:

  1. When does the console.log run?
  2. How many times if you click the button 3 times?
  3. What if the dependency array was []?

Exercise 2: Fix the Bug

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

  useEffect(() => {
    setInterval(() => {
      setSeconds(seconds + 1);
    }, 1000);
  }, []);

  return <div>{seconds} seconds</div>;
}

Problems:

  1. Missing cleanup - interval never stops
  2. Stale closure - seconds is always 0
  3. New interval created on every render

Key Takeaways

You've learned the fundamentals!

Understanding Side Effects:

  • Side effects reach outside your component
  • Common examples: API calls, timers, subscriptions
  • useEffect lets you control when side effects run

useEffect Syntax:

  • Effect function contains your side effect code
  • Dependency array controls when it runs
  • Cleanup function runs before re-running or unmounting

Best Practices:

  • Always include dependencies
  • Clean up side effects (timers, listeners, subscriptions)
  • Don't use useEffect for derived state or event handlers
  • Use ESLint plugin to catch mistakes

Next Up: In the next lesson, we'll set up a mock API and use useEffect to fetch real data for our StaySense listings!

What's Next?

Now that you understand the basics of useEffect, you're ready to use it for real-world scenarios. In the next lesson, you'll:

  • 🛠️ Create a mock API for listing data
  • 📡 Use useEffect to fetch data on component mount
  • ⚡ Learn async/await patterns with useEffect
  • 🔄 Update your HomePage to load data dynamically

Ready to fetch some data? Let's go! 🚀