C
React/Hooks/Lesson 09

useEffect

35 min·theory
This chapter
1/5
JavaScript

useEffect

💡 Why should you learn this?

🎯 This is the most fundamental way to fetch data from a server.
💼 You can control whether it runs only once on page load or only when a specific value changes.
It is an essential feature that every React developer uses on a daily basis.
🏢 실무에서는
In real-world projects, when a component appears on screen, `useEffect` is used to call an API and retrieve data such as product lists and user information. Nearly every web app is built around this pattern.

useEffect — Component lifecycle, API calls, and timers

Real-world analogy: an entrance/exit attendant

A Hook that registers code to run automatically when a component appears on screen (mount) and when it disappears (unmount).

Why learn it?

  • Data loading (API calls)
  • Setting up timers
  • Registering/removing event listeners

Basic structure

js
useEffect(() => {
  // Code to run

  return () => {
    // Cleanup code — runs on unmount
  };
}, [dependency array]);

Dependency array patterns

ArrayWhen it runs
NoneAfter every render
[]Once on mount only
[id]Every time id changes
💻 Fetching data on mount + cleanup on unmount
import { useState, useEffect } from 'react';

// ===== API Data Fetch Example =====
function UserList() {
  // Input: None (runs automatically on mount)
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Process: Runs only once on mount ([] dependency)
    async function fetchUsers() {
      const response = await fetch('https://jsonplaceholder.typicode.com/users');
      const data = await response.json();
      setUsers(data);
      setLoading(false);
    }
    fetchUsers();
  }, []);  // ← Empty array: only once on mount

  // Output: Loading or User List
  if (loading) return <p>Loading...</p>;
  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

// ===== Timer + Cleanup Example =====
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // Process: Increment count every 1 second
    const timer = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // Cleanup: Remove timer when component unmounts (prevent memory leak)
    return () => clearInterval(timer);
  }, []);  // Set only once on mount

  return <p>Elapsed Time: {seconds} seconds</p>;
}
💻 Good example — 2025 recommended pattern (AbortController + precise dependencies)
// ✅ Best Practice: React 19 + 2025 Pattern
const UserProfile = ({ userId }: { userId: string }) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const abortController = new AbortController();
    
    const loadUser = async () => {
      try {
        setLoading(true);
        setError(null);
        
        // Resolve race conditions with AbortController
        const userData = await fetchUser(userId, {
          signal: abortController.signal
        });
        
        // Abort if component unmounted or another request started
        if (!abortController.signal.aborted) {
          setUser(userData);
        }
      } catch (err) {
        if (!abortController.signal.aborted) {
          setError('Failed to load user information.');
        }
      } finally {
        if (!abortController.signal.aborted) {
          setLoading(false);
        }
      }
    };

    loadUser();

    // Cleanup function: runs on component unmount or userId change
    return () => {
      abortController.abort();
    };
  }, [userId]); // Explicit userId dependency

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>{user?.name}</div>;
};
💻 Advanced example — Improving reusability with a custom Hook
// ✅ 2025 Advanced Pattern: Custom Hook + Suspense Compatible
function useAsyncData<T>(fetchFn: () => Promise<T>, deps: any[]) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let cancelled = false;
    
    const loadData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const result = await fetchFn();
        
        if (!cancelled) {
          setData(result);
          setLoading(false);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err as Error);
          setLoading(false);
        }
      }
    };

    loadData();

    return () => {
      cancelled = true;
    };
  }, deps);

  return { data, loading, error };
}

// Usage example
const UserProfile = ({ userId }: { userId: string }) => {
  const { data: user, loading, error } = useAsyncData(
    () => fetchUser(userId),
    [userId]
  );

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

💡 ⚠️ Common mistakes

  • Omitting required values from the dependency array and ignoring ESLint warnings (violation of the exhaustive-deps rule)
  • Using setInterval, addEventListener, etc. without a cleanup function, causing memory leaks
  • Using async/await directly in the useEffect callback (useEffect should only return a cleanup function)
  • Infinite re-renders caused by reference equality issues when using objects or arrays as dependencies

💡 🎯 Interview prep

Q: Explain the difference between an empty dependency array [] and no dependency array in useEffect.
Q: How can you prevent race conditions when making API calls inside useEffect?
Q: When does the useEffect cleanup function run, and why is it necessary?

Hint: Clearly distinguish the three dependency array patterns (none / empty array / with values), and explain how to resolve race conditions using AbortController or a cleanup flag with code examples. Answer with real-world cases illustrating why cleanup matters for preventing memory leaks.

⚛️ React pattern — useEffect

Learn how useEffect is used in React step by step, with code examples.
1 🎯 1. Defining an Effect
A side effect to run after render
useEffect(() => {
  console.log('Mount or count changed');
}, [count]);
2 📋 2. Dependency array
[] = once only, [x] = when x changes, none = every render
3 🧹 3. Cleanup function
Return a cleanup function — runs on unmount
useEffect(() => {
  const t = setInterval(...);
  return () => clearInterval(t);  // cleanup
}, []);
4 ⚠️ 4. Common mistakes
Missing dependencies → stale closure / infinite loop

⏰ useEffect — mount + dependencies + cleanup

Edit the code directly and changes are reflected automatically after 0.7 seconds. Runs instantly in the browser with React 18 + Babel.
🖥️ Result — rendered React component
✏️ React 코드 수정하기 (클릭해서 열기)
⚛️ React 18 + Babel Standalone — see the result first, then edit the code yourself.

Check your understanding

When the useEffect dependency array is `[]` (an empty array), when does it run?
💡 An empty array `[]` means 'there are no dependencies.' Therefore, it runs exactly once when the component first mounts. This is the most commonly used pattern for loading initial API data!
useEffect - React