Learn React in 10 DaysDay 6: Effects and useEffect
books.chapter 6Learn React in 10 Days

Day 6: Effects and useEffect

What You'll Learn Today

  • What side effects are
  • Basic usage of the useEffect hook
  • Understanding the dependency array
  • Cleanup functions
  • Implementing data fetching

What Are Side Effects?

Side effects are operations performed outside of component rendering.

flowchart TB
    subgraph Main["Two Types of Component Operations"]
        Render["Rendering<br/>Calculate and return UI"]
        Effect["Side Effects<br/>Interact with external systems"]
    end

    subgraph Examples["Side Effect Examples"]
        E1["Fetching data from API"]
        E2["Setting up timers"]
        E3["DOM manipulation"]
        E4["Local storage"]
        E5["Event listeners"]
    end

    Effect --> Examples

    style Render fill:#3b82f6,color:#fff
    style Effect fill:#22c55e,color:#fff

Why Separate Side Effects?

// ❌ Bad: Side effect during render
function BadComponent() {
  // This runs on every render!
  fetch('/api/data')
    .then(res => res.json())
    .then(data => console.log(data));

  return <div>...</div>;
}

// βœ… Good: Side effect in useEffect
function GoodComponent() {
  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(data => console.log(data));
  }, []);

  return <div>...</div>;
}

useEffect Basics

useEffect is a hook for executing side effects.

Basic Syntax

import { useEffect } from 'react';

useEffect(() => {
  // Side effect code
}, [dependencies]);

Execution Timing with Dependency Array

flowchart TB
    subgraph Timing["useEffect Execution Timing"]
        A["No dependency array<br/>Every render"]
        B["Empty array []<br/>Mount only"]
        C["With dependencies [a, b]<br/>When a or b changes"]
    end

    style A fill:#ef4444,color:#fff
    style B fill:#3b82f6,color:#fff
    style C fill:#22c55e,color:#fff

Pattern 1: Run After Every Render

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

  // No dependency array β†’ runs every time
  useEffect(() => {
    console.log('Component rendered');
  });

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

Pattern 2: Run Only on Mount

function App() {
  const [data, setData] = useState(null);

  // Empty dependency array β†’ mount only
  useEffect(() => {
    console.log('Component mounted');
    fetch('/api/data')
      .then(res => res.json())
      .then(setData);
  }, []);

  return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

Pattern 3: Run When Specific Values Change

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

  // Runs when userId changes
  useEffect(() => {
    console.log(`Fetching user ${userId}`);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);

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

Dependency Array Details

The dependency array should include all "reactive values" used in the effect.

What Are Reactive Values?

function SearchResults({ query }) {  // props β†’ reactive
  const [page, setPage] = useState(1);  // state β†’ reactive
  const limit = 10;  // constant β†’ not reactive

  useEffect(() => {
    // Uses query and page β†’ include in dependencies
    fetch(`/api/search?q=${query}&page=${page}&limit=${limit}`)
      .then(res => res.json())
      .then(data => console.log(data));
  }, [query, page]);  // limit is constant, not needed

  return (...);
}

Common Mistakes

// ❌ Missing dependency
function BadExample({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)  // uses userId
      .then(res => res.json())
      .then(setUser);
  }, []);  // userId missing from dependencies!

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

// βœ… Correct dependencies
function GoodExample({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);  // userId included

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

Cleanup Functions

Some side effects require cleanup.

flowchart TB
    subgraph Lifecycle["Effect Lifecycle"]
        A["Mount"]
        B["Effect runs"]
        C["Re-render"]
        D["Cleanup runs"]
        E["New effect runs"]
        F["Unmount"]
        G["Final cleanup"]
    end

    A --> B --> C --> D --> E
    C --> F --> G

    style B fill:#22c55e,color:#fff
    style D fill:#f59e0b,color:#fff
    style G fill:#ef4444,color:#fff

Timer Cleanup

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

  useEffect(() => {
    const intervalId = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // Return cleanup function
    return () => {
      clearInterval(intervalId);
      console.log('Timer cleared');
    };
  }, []);

  return <div>Elapsed: {seconds} seconds</div>;
}
TypeScript version
function Timer() {
  const [seconds, setSeconds] = useState<number>(0);

  useEffect(() => {
    const intervalId: ReturnType<typeof setInterval> = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // Return cleanup function
    return () => {
      clearInterval(intervalId);
      console.log('Timer cleared');
    };
  }, []);

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

Event Listener Cleanup

function WindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    function handleResize() {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    }

    window.addEventListener('resize', handleResize);

    // Cleanup
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return (
    <div>
      Window size: {size.width} x {size.height}
    </div>
  );
}
TypeScript version
interface WindowDimensions {
  width: number;
  height: number;
}

function WindowSize() {
  const [size, setSize] = useState<WindowDimensions>({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    function handleResize(): void {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    }

    window.addEventListener('resize', handleResize);

    // Cleanup
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return (
    <div>
      Window size: {size.width} x {size.height}
    </div>
  );
}

WebSocket Cleanup

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const socket = new WebSocket(`wss://chat.example.com/${roomId}`);

    socket.onmessage = (event) => {
      const message = JSON.parse(event.data);
      setMessages(prev => [...prev, message]);
    };

    // Cleanup: close connection
    return () => {
      socket.close();
    };
  }, [roomId]);

  return (
    <ul>
      {messages.map((msg, i) => <li key={i}>{msg.text}</li>)}
    </ul>
  );
}
TypeScript version
interface ChatMessage {
  text: string;
}

interface ChatRoomProps {
  roomId: string;
}

function ChatRoom({ roomId }: ChatRoomProps) {
  const [messages, setMessages] = useState<ChatMessage[]>([]);

  useEffect(() => {
    const socket = new WebSocket(`wss://chat.example.com/${roomId}`);

    socket.onmessage = (event: MessageEvent) => {
      const message: ChatMessage = JSON.parse(event.data);
      setMessages(prev => [...prev, message]);
    };

    // Cleanup: close connection
    return () => {
      socket.close();
    };
  }, [roomId]);

  return (
    <ul>
      {messages.map((msg, i) => <li key={i}>{msg.text}</li>)}
    </ul>
  );
}

Data Fetching

A common pattern for fetching data from APIs.

Basic Data Fetching

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchUsers() {
      try {
        setLoading(true);
        const response = await fetch('/api/users');
        if (!response.ok) {
          throw new Error('Failed to fetch data');
        }
        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    fetchUsers();
  }, []);

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

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
TypeScript version
interface User {
  id: number;
  name: string;
}

function UserList() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function fetchUsers(): Promise<void> {
      try {
        setLoading(true);
        const response = await fetch('/api/users');
        if (!response.ok) {
          throw new Error('Failed to fetch data');
        }
        const data: User[] = await response.json();
        setUsers(data);
      } catch (err) {
        setError((err as Error).message);
      } finally {
        setLoading(false);
      }
    }

    fetchUsers();
  }, []);

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

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Handling Race Conditions

Prevent state updates after component unmounts.

function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    let isCancelled = false;

    async function search() {
      const response = await fetch(`/api/search?q=${query}`);
      const data = await response.json();

      // Only update if not cancelled
      if (!isCancelled) {
        setResults(data);
      }
    }

    search();

    // Set cancel flag in cleanup
    return () => {
      isCancelled = true;
    };
  }, [query]);

  return (
    <ul>
      {results.map(item => <li key={item.id}>{item.title}</li>)}
    </ul>
  );
}
TypeScript version
interface SearchResult {
  id: number;
  title: string;
}

interface SearchResultsProps {
  query: string;
}

function SearchResults({ query }: SearchResultsProps) {
  const [results, setResults] = useState<SearchResult[]>([]);

  useEffect(() => {
    let isCancelled = false;

    async function search(): Promise<void> {
      const response = await fetch(`/api/search?q=${query}`);
      const data: SearchResult[] = await response.json();

      // Only update if not cancelled
      if (!isCancelled) {
        setResults(data);
      }
    }

    search();

    // Set cancel flag in cleanup
    return () => {
      isCancelled = true;
    };
  }, [query]);

  return (
    <ul>
      {results.map(item => <li key={item.id}>{item.title}</li>)}
    </ul>
  );
}

Using AbortController

function FetchWithAbort({ url }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

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

    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url, {
          signal: controller.signal
        });
        const json = await response.json();
        setData(json);
      } catch (err) {
        if (err.name !== 'AbortError') {
          console.error('Fetch error:', err);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchData();

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

  if (loading) return <div>Loading...</div>;
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
TypeScript version
interface FetchWithAbortProps {
  url: string;
}

function FetchWithAbort({ url }: FetchWithAbortProps) {
  const [data, setData] = useState<unknown>(null);
  const [loading, setLoading] = useState<boolean>(true);

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

    async function fetchData(): Promise<void> {
      try {
        setLoading(true);
        const response = await fetch(url, {
          signal: controller.signal
        });
        const json: unknown = await response.json();
        setData(json);
      } catch (err) {
        if ((err as Error).name !== 'AbortError') {
          console.error('Fetch error:', err);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchData();

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

  if (loading) return <div>Loading...</div>;
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

Extracting to Custom Hooks

Extract data fetching logic into a custom hook.

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

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

    async function fetchData() {
      try {
        setLoading(true);
        setError(null);
        const response = await fetch(url, {
          signal: controller.signal
        });
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const json = await response.json();
        setData(json);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchData();

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

  return { data, loading, error };
}

// Usage
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);

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

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}
TypeScript version
interface UseFetchResult<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

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

    async function fetchData(): Promise<void> {
      try {
        setLoading(true);
        setError(null);
        const response = await fetch(url, {
          signal: controller.signal
        });
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const json = await response.json() as T;
        setData(json);
      } catch (err) {
        if ((err as Error).name !== 'AbortError') {
          setError((err as Error).message);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchData();

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

  return { data, loading, error };
}

// Usage
interface User {
  name: string;
  email: string;
}

interface UserProfileProps {
  userId: string;
}

function UserProfile({ userId }: UserProfileProps) {
  const { data: user, loading, error } = useFetch<User>(`/api/users/${userId}`);

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

  return (
    <div>
      <h1>{user?.name}</h1>
      <p>{user?.email}</p>
    </div>
  );
}

useEffect Best Practices

Avoid Unnecessary Effects

// ❌ Unnecessary effect: derived value calculation
function BadExample({ items }) {
  const [total, setTotal] = useState(0);

  useEffect(() => {
    setTotal(items.reduce((sum, item) => sum + item.price, 0));
  }, [items]);

  return <div>Total: {total}</div>;
}

// βœ… Calculate during render
function GoodExample({ items }) {
  const total = items.reduce((sum, item) => sum + item.price, 0);

  return <div>Total: {total}</div>;
}

Event Handlers vs useEffect

// ❌ Using useEffect for form submission
function BadForm() {
  const [submitted, setSubmitted] = useState(false);

  useEffect(() => {
    if (submitted) {
      fetch('/api/submit', { method: 'POST' });
    }
  }, [submitted]);

  return <button onClick={() => setSubmitted(true)}>Submit</button>;
}

// βœ… Use event handler
function GoodForm() {
  function handleSubmit() {
    fetch('/api/submit', { method: 'POST' });
  }

  return <button onClick={handleSubmit}>Submit</button>;
}

Summary

Concept Description
Side effects Operations outside rendering (API calls, timers, etc.)
useEffect Hook for executing side effects
Dependency array Controls when effect runs
Cleanup Clean up after effects (clear timers, etc.)
Race conditions Prevent competing async operations

Key Takeaways

  1. Include all reactive values used in the effect in the dependency array
  2. Use cleanup functions to release resources
  3. Implement cancellation to prevent race conditions
  4. Calculate derived values during render, not in useEffect
  5. Handle event responses in event handlers, not useEffect

Exercises

Exercise 1: Basics

Create a Clock component that updates and displays the current time every second.

Exercise 2: Application

Create a component that fetches data from an API when a search query is entered. Display loading and error states.

Challenge

Create a ScrollProgress component that monitors window scroll position and displays what percentage of the page has been scrolled.


References


Coming Up Next: On Day 7, we'll learn about "Refs and Portals." Understand direct DOM access and rendering outside the DOM tree.