React

React useEffect — Side Effects & Data Fetching

Thirdy Gayares
22 min read

🎓 What You Will Learn

  • What is useEffect: Handle side effects in functional components
  • Run Once on Mount: Use empty dependency array []
  • Dependencies: Control when effect runs
  • Cleanup Functions: Clean up resources and listeners
  • Data Fetching: Fetch API data when component mounts
  • Multiple Effects: Use many useEffect calls
  • Common Patterns: Timers, listeners, API calls
  • Best Practices: Avoid common mistakes
1

What is useEffect?

useEffect is a React Hook that lets you perform side effects in functional components. Side effects are operations that interact with the outside world: fetching data, updating the DOM, setting up timers, listening to events, etc.

Without useEffect, side effects would run every time the component renders, causing performance issues and bugs. useEffect lets you control when side effects run.

Side Effects Include:
  • • Fetching data from APIs
  • • Setting up event listeners
  • • Updating the document title
  • • Starting timers/intervals
  • • Saving to localStorage
2

Basic Syntax

useEffect-syntax.jsx
import { useEffect } from 'react';

export function MyComponent() {
  useEffect(() => {
    // Side effect code here
    console.log("Effect ran!");

    // Optional cleanup function
    return () => {
      console.log("Cleanup!");
    };
  }, []);  // Dependency array
  //^       Controls when effect runs

  return <p>Component</p>;
}
▶ Try it YourselfRun Once on Mount

Effect runs ONE time when component mounts:

Logs will appear here...

👉 Click the button multiple times. The effect only logged once on mount!

BasicEffect.jsx
import { useState, useEffect } from 'react';

export function EffectOnce() {
  const [count, setCount] = useState(0);
  const [logs, setLogs] = useState([]);

  // Runs ONCE when component mounts
  useEffect(() => {
    setLogs(prev => [...prev, 'Effect ran!']);
  }, []);  // Empty dependency array = run once

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        Click ({count})
      </button>
      {logs.map((log, i) => <p key={i}>{log}</p>)}
    </>
  );
}
3

The Dependency Array

The dependency array controls when useEffect runs:

Dependency ArrayWhen It RunsUse Case
No array providedEvery render❌ Usually wrong, causes bugs
Empty <Code>[]</Code>Once on mount✅ Initialize, fetch data
<Code>[count]</Code>When count changes✅ React to specific state
<Code>[count, text]</Code>When count or text changes✅ Multiple dependencies
▶ Try it YourselfDependencies

Effect runs when count changes:

👉 Type in the message input. Effect doesn't run! Change count, it runs!

Dependencies.jsx
import { useState, useEffect } from 'react';

export function DependentEffect() {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState("");

  // Runs when COUNT changes (not when message changes)
  useEffect(() => {
    console.log("Count changed:", count);
  }, [count]);  // Dependency array

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
      <input
        value={message}
        onChange={(e) => setMessage(e.target.value)}
      />
    </>
  );
}
4

Cleanup Functions

Return a function from useEffect to clean up resources. This runs when:

  • • The component unmounts
  • • Before the effect runs again
cleanup.jsx
useEffect(() => {
  const timer = setInterval(() => {
    console.log("Tick");
  }, 1000);

  // Cleanup function
  return () => {
    clearInterval(timer);  // Clean up timer
  };
}, []);
▶ Try it YourselfCleanup Function

Event listener with cleanup:

❌ Listener OFF

👉 Turn ON and press any key. Turn OFF to remove listener. Check console!

Cleanup.jsx
import { useState, useEffect } from 'react';

export function Listener() {
  const [isOn, setIsOn] = useState(false);

  useEffect(() => {
    if (!isOn) return;

    const handleKey = () => console.log("Key pressed!");

    // Setup
    window.addEventListener("keypress", handleKey);

    // Cleanup function - runs when component unmounts
    // or when isOn becomes false
    return () => {
      window.removeEventListener("keypress", handleKey);
      console.log("Listener removed!");
    };
  }, [isOn]);

  return (
    <button onClick={() => setIsOn(!isOn)}>
      {isOn ? "Turn OFF" : "Turn ON"}
    </button>
  );
}
5

Multiple useEffect Calls

Use multiple useEffect calls for different concerns. Each effect handles one thing.

multiple-effects.jsx
export function MyComponent() {
  // Effect 1: Fetch data
  useEffect(() => {
    fetch('/api/data');
  }, []);

  // Effect 2: Set up event listener
  useEffect(() => {
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  // Effect 3: Sync to localStorage
  useEffect(() => {
    localStorage.setItem('name', name);
  }, [name]);
}
▶ Try it YourselfMultiple Effects

Each effect has its own dependency:

👉 Change count (Effect 1 runs). Change text (Effect 2 runs).

MultipleEffects.jsx
import { useState, useEffect } from 'react';

export function MultipleEffects() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // Effect 1: Runs when count changes
  useEffect(() => {
    console.log("Count changed:", count);
  }, [count]);

  // Effect 2: Runs when text changes
  useEffect(() => {
    console.log("Text changed:", text);
  }, [text]);

  return (
    <>
      <input
        type="number"
        value={count}
        onChange={(e) => setCount(Number(e.target.value))}
      />
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
    </>
  );
}
6

Fetching Data from APIs

The most common use of useEffect is fetching data when the component mounts.

fetch-data.jsx
export function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        setUsers(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, []);  // Run once on mount

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

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
▶ Try it YourselfFetch Data

Simulated API fetch:

👉 Click different user buttons. Effect fetches data when id changes!

FetchData.jsx
import { useState, useEffect } from 'react';

export function UserFetcher() {
  const [id, setId] = useState(1);
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);

    // Fetch user data
    fetch(`/api/users/${id}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, [id]);  // Re-fetch when id changes

  return (
    <>
      <button onClick={() => setId(1)}>User 1</button>
      <button onClick={() => setId(2)}>User 2</button>
      {loading && <p>Loading...</p>}
      {user && <p>{user.name}</p>}
    </>
  );
}
7

Common Patterns

1Fetch on Mount
fetch-pattern.jsx
useEffect(() => {
  fetch('/api/data')
    .then(res => res.json())
    .then(data => setData(data));
}, []);  // Empty dependency = fetch once
2Re-fetch on Change
refetch-pattern.jsx
useEffect(() => {
  fetch(`/api/users/${id}`)
    .then(res => res.json())
    .then(data => setUser(data));
}, [id]);  // Re-fetch when id changes
3Event Listener
listener-pattern.jsx
useEffect(() => {
  const handleResize = () => {
    console.log('Window resized');
  };

  window.addEventListener('resize', handleResize);

  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);
▶ Try it YourselfDocument Title

Change browser tab title:

Current Title: Welcome - My App

👉 Click buttons and watch the browser tab title change!

DocumentTitle.jsx
import { useState, useEffect } from 'react';

export function TabTitle() {
  const [title, setTitle] = useState("Welcome");

  useEffect(() => {
    document.title = title;
  }, [title]);

  return (
    <>
      <button onClick={() => setTitle("Settings")}>
        Settings
      </button>
      <p>Current: {title}</p>
    </>
  );
}
8

Best Practices

Always include dependency array: Or side effects run every render
Include all dependencies: Variables used in effect
Return cleanup function: For event listeners, timers, subscriptions
Use multiple useEffect calls: Separate concerns
Handle errors: In data fetching
9

Common Mistakes

1Mistake: No Dependency Array
mistake-no-deps.jsx
// ❌ BAD: Runs every render = infinite loop
useEffect(() => {
  fetch('/api/data');
  // Missing dependency array!
});

// ✅ GOOD: Runs once on mount
useEffect(() => {
  fetch('/api/data');
}, []);
2Mistake: Missing Dependencies
mistake-missing-deps.jsx
// ❌ BAD: count used but not in dependencies
useEffect(() => {
  console.log(count);
}, []);  // Missing count!

// ✅ GOOD: Include all dependencies
useEffect(() => {
  console.log(count);
}, [count]);
3Mistake: Forgetting Cleanup
mistake-no-cleanup.jsx
// ❌ BAD: Memory leak - listener never removed
useEffect(() => {
  window.addEventListener('resize', handleResize);
}, []);

// ✅ GOOD: Clean up listeners
useEffect(() => {
  window.addEventListener('resize', handleResize);
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);
10

What's Next?

🚀 Next Topics to Learn:
  • Custom Hooks: Build reusable effect logic (useFetch, useLocalStorage)
  • useCallback & useMemo: Optimize useEffect dependencies
  • useContext: Share data without prop drilling
  • useReducer: Complex state logic in effects
  • API Libraries: Axios, React Query, SWR for better data fetching

Experiment with the examples! 💪 useEffect is powerful but takes practice to master. Build apps with API calls, listeners, and timers to solidify your understanding!

About the Author

TG

Thirdy Gayares

Passionate developer creating custom solutions for everyone. I specialize in building user-friendly tools that solve real-world problems while maintaining the highest standards of security and privacy.