You are viewing a preview of this course. Sign in to start learning

Lesson 3: useEffect and Side Effects

Master the useEffect hook to handle side effects, API calls, and component lifecycle in React applications

Lesson 3: useEffect and Side Effects πŸ’»

Introduction

Welcome back! In the previous lessons, you learned about React Hooks basics and how to manage state with useState. Now it's time to tackle one of the most powerful and essential hooks: useEffect. This hook opens the door to handling side effects in your React componentsβ€”everything from fetching data from APIs to setting up subscriptions, manipulating the DOM directly, and managing timers.

πŸ€” Did you know? Before hooks, handling side effects required class components with lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount. The useEffect hook elegantly combines all three into a single, intuitive API!

What Are Side Effects? 🌊

Before diving into useEffect, let's clarify what we mean by side effects. In programming, a side effect is any operation that affects something outside the scope of the function being executed.

Common Side Effects in React:

  • πŸ“‘ Fetching data from an API
  • πŸ”” Setting up subscriptions (WebSockets, event listeners)
  • ⏰ Timers and intervals (setTimeout, setInterval)
  • πŸ“ Directly manipulating the DOM (changing document.title)
  • πŸ’Ύ Reading/writing to localStorage
  • πŸ“Š Logging or analytics

The Pure Function Principle

React components should ideally be pure functions during renderingβ€”given the same props and state, they should return the same JSX. Side effects break this purity, which is why we need a dedicated place to handle them.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     COMPONENT RENDERING CYCLE       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                     β”‚
β”‚  Props + State β†’ Pure Function      β”‚
β”‚                      ↓              β”‚
β”‚                    JSX              β”‚
β”‚                                     β”‚
β”‚  Side Effects β†’ useEffect Hook      β”‚
β”‚                                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ’‘ Think of it this way: Your component's render function is like cooking a mealβ€”it transforms ingredients (props/state) into a finished dish (JSX). Side effects are like cleaning up afterward or ordering more ingredientsβ€”necessary, but separate from the cooking itself!

The useEffect Hook: Core Syntax 🎯

The useEffect hook accepts two arguments:

useEffect(() => {
  // Side effect code goes here
  
  return () => {
    // Cleanup function (optional)
  };
}, [dependencies]);

Breaking Down the Anatomy:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  useEffect(effectFunction, dependencyArray)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚                    β”‚
           β”‚                    └─→ When to run
           β”‚
           └─→ What to run

  Effect Function:
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  () => {                    β”‚
  β”‚    // Execute side effect   β”‚
  β”‚    return () => {           β”‚
  β”‚      // Cleanup             β”‚
  β”‚    };                       β”‚
  β”‚  }                          β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  Dependency Array:
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  []           β†’ Run once (mount only)    β”‚
  β”‚  [a, b]       β†’ Run when a or b changes  β”‚
  β”‚  (undefined)  β†’ Run after every render   β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Understanding the Dependency Array 🎲

The dependency array is crucialβ€”it determines when your effect runs. This is where many beginners struggle, so let's break it down thoroughly.

Three Scenarios:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Dependency Array Behavior                              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Pattern         β”‚  When Effect Runs                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  useEffect(fn)   β”‚  After EVERY render                  β”‚
β”‚                  β”‚  ⚠️ Usually too often!               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  useEffect(fn,[])β”‚  Only on mount (first render)        β”‚
β”‚                  β”‚  βœ… Good for initial setup           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  useEffect(fn,   β”‚  On mount + when dependencies change β”‚
β”‚    [a, b])       β”‚  βœ… Most common pattern              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Example: Count Tracking

import { useState, useEffect } from 'react';

function CounterLogger() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Guest');

  // Runs after EVERY render
  useEffect(() => {
    console.log('Component rendered!');
  });

  // Runs only on mount
  useEffect(() => {
    console.log('Component mounted!');
  }, []);

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

  // Runs when count OR name changes
  useEffect(() => {
    console.log(`Either count or name changed!`);
  }, [count, name]);

  return (
    <div>
      <p>{name}: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setName('User')}>Change Name</button>
    </div>
  );
}

πŸ’‘ Tip: Always include all values from the component scope that your effect uses in the dependency array. Modern React linters will warn you if you forget!

Real-World Examples 🌍

Example 1: Fetching Data from an API πŸ“‘

One of the most common use cases for useEffect is fetching data when a component mounts.

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    // Reset states 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 user');
        }
        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>Email: {user.email}</p>
    </div>
  );
}

What's happening here?

  1. When the component mounts or userId changes, the effect runs
  2. We set loading to true and clear any previous errors
  3. We fetch the user data from the API
  4. On success, we update the user state and set loading to false
  5. On error, we capture the error message
  6. The component re-renders based on the new state
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Data Fetching Flow             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                        β”‚
β”‚  Component Mounts                      β”‚
β”‚         ↓                              β”‚
β”‚  useEffect Triggers                    β”‚
β”‚         ↓                              β”‚
β”‚  setLoading(true)                      β”‚
β”‚         ↓                              β”‚
β”‚  API Request                           β”‚
β”‚         ↓                              β”‚
β”‚    β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”                         β”‚
β”‚  Success   Error                       β”‚
β”‚    β”‚         β”‚                         β”‚
β”‚  setUser  setError                     β”‚
β”‚    β”‚         β”‚                         β”‚
β”‚    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜                         β”‚
β”‚         ↓                              β”‚
β”‚  setLoading(false)                     β”‚
β”‚         ↓                              β”‚
β”‚  Re-render with Data                   β”‚
β”‚                                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Example 2: Cleanup Functions 🧹

Some side effects need cleanup to prevent memory leaks. The cleanup function runs when:

  • The component unmounts
  • Before the effect runs again (if dependencies changed)
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    if (!isRunning) return; // Don't set up interval if not running

    // Set up the interval
    const intervalId = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // Cleanup function: runs before next effect or unmount
    return () => {
      clearInterval(intervalId);
      console.log('Interval cleared!');
    };
  }, [isRunning]); // Re-run effect when isRunning changes

  return (
    <div>
      <h2>Timer: {seconds} seconds</h2>
      <button onClick={() => setIsRunning(!isRunning)}>
        {isRunning ? 'Pause' : 'Resume'}
      </button>
    </div>
  );
}

Why cleanup matters:

Without the cleanup function, every time isRunning changes, a new interval would be created without clearing the old one. This causes:

  • ⚠️ Multiple intervals running simultaneously
  • ⚠️ Memory leaks
  • ⚠️ Unpredictable behavior

Example 3: Document Title Updates πŸ“

A simple but practical example: updating the browser tab title based on component state.

import { useState, useEffect } from 'react';

function MessageCounter() {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    // Update document title
    if (messages.length === 0) {
      document.title = 'No new messages';
    } else {
      document.title = `(${messages.length}) New Messages`;
    }

    // Cleanup: reset title when component unmounts
    return () => {
      document.title = 'My App';
    };
  }, [messages.length]); // Only re-run when message count changes

  const addMessage = () => {
    setMessages([...messages, `Message ${messages.length + 1}`]);
  };

  return (
    <div>
      <h2>You have {messages.length} messages</h2>
      <button onClick={addMessage}>Add Message</button>
      <ul>
        {messages.map((msg, index) => (
          <li key={index}>{msg}</li>
        ))}
      </ul>
    </div>
  );
}

Example 4: Event Listeners 🎯

Setting up and cleaning up event listeners is another common pattern.

import { useState, useEffect } from 'react';

function MouseTracker() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    // Event handler function
    const handleMouseMove = (event) => {
      setPosition({
        x: event.clientX,
        y: event.clientY
      });
    };

    // Add event listener
    window.addEventListener('mousemove', handleMouseMove);

    // Cleanup: remove event listener
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []); // Empty array: setup once on mount, cleanup on unmount

  return (
    <div>
      <h2>Mouse Position</h2>
      <p>X: {position.x}, Y: {position.y}</p>
    </div>
  );
}

Key Pattern: Notice how we:

  1. Define the handler function inside the effect
  2. Add the event listener
  3. Return a cleanup function that removes the listener
  4. Use an empty dependency array since this setup doesn't depend on any props or state

Common Mistakes and How to Avoid Them ⚠️

Mistake 1: Missing Dependencies

// ❌ WRONG: Missing userId in dependencies
function BadExample({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, []); // Bug: won't re-fetch when userId changes!
  
  return <div>{user?.name}</div>;
}

// βœ… CORRECT: Include all dependencies
function GoodExample({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]); // Now re-fetches when userId changes
  
  return <div>{user?.name}</div>;
}

πŸ’‘ Rule of thumb: If your effect uses a value from props or state, include it in the dependency array!

Mistake 2: Infinite Loops πŸ”„

// ❌ WRONG: Creates infinite loop
function BadCounter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    setCount(count + 1); // Changes count...
  }, [count]); // ...which triggers effect again!
  
  return <div>{count}</div>;
}

// βœ… CORRECT: Use empty array or different logic
function GoodCounter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    // Runs only once on mount
    const timer = setTimeout(() => setCount(1), 1000);
    return () => clearTimeout(timer);
  }, []);
  
  return <div>{count}</div>;
}

Mistake 3: Not Cleaning Up Resources

// ❌ WRONG: Memory leak
function BadTimer() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    // No cleanup! Interval keeps running even after unmount
  }, []);
  
  return <div>{count}</div>;
}

// βœ… CORRECT: Proper cleanup
function GoodTimer() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    
    return () => clearInterval(id); // Clean up!
  }, []);
  
  return <div>{count}</div>;
}

Mistake 4: Async Functions in useEffect

// ❌ WRONG: Can't make effect function async directly
function BadAsync() {
  const [data, setData] = useState(null);
  
  useEffect(async () => { // ❌ Error!
    const response = await fetch('/api/data');
    const result = await response.json();
    setData(result);
  }, []);
  
  return <div>{data}</div>;
}

// βœ… CORRECT: Define async function inside
function GoodAsync() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('/api/data');
      const result = await response.json();
      setData(result);
    };
    
    fetchData();
  }, []);
  
  return <div>{data}</div>;
}

// βœ… ALSO CORRECT: Use .then() instead
function AlsoGoodAsync() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(setData);
  }, []);
  
  return <div>{data}</div>;
}

Why? The effect function must return either nothing or a cleanup function, not a Promise!

Advanced Pattern: Cancelling Requests 🚫

When fetching data, the component might unmount before the request completes. This can cause warnings about updating unmounted components.

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

  useEffect(() => {
    let cancelled = false; // Flag to track if effect was cleaned up

    setLoading(true);
    
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) { // Only update if still mounted
          setUser(data);
          setLoading(false);
        }
      });

    return () => {
      cancelled = true; // Set flag on cleanup
    };
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  return <div>{user?.name}</div>;
}

🧠 Modern approach: Use the AbortController API:

function ModernDataFetcher({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/users/${userId}`, {
      signal: controller.signal
    })
      .then(res => res.json())
      .then(setUser)
      .catch(error => {
        if (error.name !== 'AbortError') {
          console.error('Fetch error:', error);
        }
      });

    return () => controller.abort();
  }, [userId]);

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

When to Use useEffect vs. Other Solutions πŸ€”

Not everything needs useEffect! Here's a decision guide:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Do you need useEffect?                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                     β”‚
β”‚  Calculating values from props/state?               β”‚
β”‚    β†’ NO: Just use regular variables                 β”‚
β”‚                                                     β”‚
β”‚  Updating state based on props?                     β”‚
β”‚    β†’ MAYBE: Consider if logic can be in render      β”‚
β”‚                                                     β”‚
β”‚  Fetching data on mount/prop change?                β”‚
β”‚    β†’ YES: useEffect is appropriate                  β”‚
β”‚                                                     β”‚
β”‚  Subscribing to external events?                    β”‚
β”‚    β†’ YES: useEffect with cleanup                    β”‚
β”‚                                                     β”‚
β”‚  Responding to user events (clicks, etc.)?          β”‚
β”‚    β†’ NO: Use event handlers instead                 β”‚
β”‚                                                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Example: Don't Use Effect When You Don't Need It

// ❌ Unnecessary useEffect
function BadFullName({ firstName, lastName }) {
  const [fullName, setFullName] = useState('');
  
  useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
  }, [firstName, lastName]);
  
  return <div>{fullName}</div>;
}

// βœ… Just compute it directly
function GoodFullName({ firstName, lastName }) {
  const fullName = `${firstName} ${lastName}`;
  return <div>{fullName}</div>;
}

Key Takeaways 🎯

βœ… useEffect handles side effects that don't belong in the render logic

βœ… The dependency array controls when effects run:

  • No array: every render
  • Empty array: only on mount
  • With values: when those values change

βœ… Always clean up resources like intervals, listeners, and subscriptions

βœ… Include all dependencies your effect uses from props/state

βœ… Can't use async directly in the effect functionβ€”define async function inside

βœ… Avoid infinite loops by carefully managing dependencies

βœ… Not everything needs useEffectβ€”calculate derived values directly in render

βœ… Cleanup functions run before the next effect and on unmount

πŸ”§ Try This: Practice Exercise

Create a simple component that:

  1. Fetches a random joke from an API when mounted
  2. Has a "Get New Joke" button that fetches a new joke
  3. Shows loading state while fetching
  4. Updates the document title with the joke setup
  5. Cleans up by resetting the title on unmount

API endpoint: https://official-joke-api.appspot.com/random_joke

Quick Reference Card πŸ“‹

╔════════════════════════════════════════════════════════╗
β•‘           useEffect CHEAT SHEET                        β•‘
╠════════════════════════════════════════════════════════╣
β•‘                                                        β•‘
β•‘  Basic Syntax:                                         β•‘
β•‘  useEffect(() => {                                     β•‘
β•‘    // effect code                                      β•‘
β•‘    return () => { /* cleanup */ };                     β•‘
β•‘  }, [dependencies]);                                   β•‘
β•‘                                                        β•‘
β•‘  Common Patterns:                                      β•‘
β•‘  β€’ Data fetching:      [], [userId]                    β•‘
β•‘  β€’ Subscriptions:      [] with cleanup                 β•‘
β•‘  β€’ Timers:             [] with clearInterval/Timeout   β•‘
β•‘  β€’ Event listeners:    [] with removeEventListener     β•‘
β•‘  β€’ Document updates:   [value] to sync                 β•‘
β•‘                                                        β•‘
β•‘  Remember:                                             β•‘
β•‘  βœ“ List all dependencies                               β•‘
β•‘  βœ“ Clean up resources                                  β•‘
β•‘  βœ“ Handle async properly                               β•‘
β•‘  βœ— Don't modify state that's in dependencies           β•‘
β•‘  βœ— Don't use async as the effect function              β•‘
β•‘                                                        β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

πŸ“š Further Study

In the next lesson, we'll explore useContext and learn how to share state across multiple components without prop drilling. See you there! πŸš€