Skip to main content

Overview

Side effects are operations that reach outside the component function - like fetching data, setting up subscriptions, or interacting with browser APIs. The useEffect hook manages these operations in functional components.

useEffect Hook

Execute side effects after render

Dependencies

Control when effects run

Cleanup Functions

Prevent memory leaks and bugs

useCallback

Optimize function dependencies

What Are Side Effects?

Side effects are any operations that affect things outside the component function scope.
  • Fetching data from APIs
  • Subscribing to events
  • Setting timers
  • Manipulating the DOM directly
  • Reading/writing localStorage
  • Logging to console
Side effects should not run during render because:
  • They can cause infinite loops
  • They make components impure
  • They can trigger unnecessary re-renders
useEffect runs after the component renders, keeping render pure.

Basic Syntax

useEffect(() => {
  // Effect code here
  
  return () => {
    // Cleanup code (optional)
  };
}, [dependencies]);
Effect Timing:
  • Effect runs after render is committed to screen
  • Cleanup runs before next effect or unmount
  • Dependencies determine when effect re-runs

Fetching Data with useEffect

Here’s a real example from the course showing geolocation and data fetching:
App.jsx
import { useRef, useState, useEffect } from 'react';
import { sortPlacesByDistance } from './loc.js';
import { AVAILABLE_PLACES } from './data.js';

function App() {
  const [availablePlaces, setAvailablePlaces] = useState([]);
  const [pickedPlaces, setPickedPlaces] = useState([]);

  useEffect(() => {
    navigator.geolocation.getCurrentPosition((position) => {
      const sortedPlaces = sortPlacesByDistance(
        AVAILABLE_PLACES,
        position.coords.latitude,
        position.coords.longitude
      );

      setAvailablePlaces(sortedPlaces);
    });
  }, []); // Empty dependency array - runs once on mount

  function handleSelectPlace(id) {
    setPickedPlaces((prevPickedPlaces) => {
      if (prevPickedPlaces.some((place) => place.id === id)) {
        return prevPickedPlaces;
      }
      const place = AVAILABLE_PLACES.find((place) => place.id === id);
      return [place, ...prevPickedPlaces];
    });
  }

  return (
    <Places
      title="Available Places"
      places={availablePlaces}
      fallbackText="Sorting places by distance..."
      onSelectPlace={handleSelectPlace}
    />
  );
}
Why useEffect Here?The navigator.geolocation API is asynchronous and should not run during render. Running it directly in the component body would:
  • Execute on every render
  • Potentially cause infinite loops
  • Not wait for the component to mount

Understanding Dependencies

The dependency array controls when your effect runs.
Effect runs after every render:
useEffect(() => {
  console.log('Runs after every render');
}); // No dependency array
Use sparingly - can cause performance issues

Cleanup Functions

Cleanup functions prevent memory leaks by canceling subscriptions, timers, and other ongoing operations.
import { useEffect } from 'react';

export default function DeleteConfirmation({ onConfirm, onCancel }) {
  useEffect(() => {
    console.log('TIMER SET');
    const timer = setTimeout(() => {
      onConfirm();
    }, 3000);

    return () => {
      console.log('Cleaning up timer');
      clearTimeout(timer);
    };
  }, [onConfirm]);

  return (
    <div id="delete-confirmation">
      <h2>Are you sure?</h2>
      <p>Do you really want to remove this place?</p>
      <div id="confirmation-actions">
        <button onClick={onCancel} className="button-text">
          No
        </button>
        <button onClick={onConfirm} className="button">
          Yes
        </button>
      </div>
    </div>
  );
}
1

Effect Runs

Timer is set when component mounts or onConfirm changes
2

Cleanup Before Re-run

Previous timer is cleared before setting new one
3

Cleanup on Unmount

Timer is cleared when component unmounts
When Cleanup Runs:
  • Before the effect runs again
  • When the component unmounts
  • This prevents memory leaks and stale operations

The useCallback Hook

When passing functions as dependencies to useEffect, use useCallback to prevent unnecessary effect re-runs.
App.jsx
import { useCallback, useEffect, useState } from 'react';

function App() {
  const [modalIsOpen, setModalIsOpen] = useState(false);
  const selectedPlace = useRef();

  // Memoize function so it doesn't change on every render
  const handleRemovePlace = useCallback(function handleRemovePlace() {
    setPickedPlaces((prevPickedPlaces) =>
      prevPickedPlaces.filter((place) => place.id !== selectedPlace.current)
    );
    setModalIsOpen(false);

    const storedIds = JSON.parse(localStorage.getItem('selectedPlaces')) || [];
    localStorage.setItem(
      'selectedPlaces',
      JSON.stringify(storedIds.filter((id) => id !== selectedPlace.current))
    );
  }, []); // Only created once

  return (
    <Modal open={modalIsOpen}>
      <DeleteConfirmation
        onConfirm={handleRemovePlace}
        onCancel={() => setModalIsOpen(false)}
      />
    </Modal>
  );
}
Functions are recreated on every render. Without useCallback:
// New function created every render
const handleClick = () => { /* ... */ };
This causes child components and effects depending on it to re-run unnecessarily.
const memoizedFn = useCallback(
  () => {
    // function body
  },
  [dependencies]
);
Returns the same function reference unless dependencies change.

Common Patterns

Pattern 1: LocalStorage Sync

useEffect(() => {
  const storedIds = JSON.parse(localStorage.getItem('selectedPlaces')) || [];
  const storedPlaces = storedIds.map((id) =>
    AVAILABLE_PLACES.find((place) => place.id === id)
  );
  setPickedPlaces(storedPlaces);
}, []); // Load once on mount

// Save on every change
function handleSelectPlace(id) {
  // Update state
  setPickedPlaces(/* ... */);
  
  // Sync to localStorage (not in useEffect)
  const storedIds = JSON.parse(localStorage.getItem('selectedPlaces')) || [];
  localStorage.setItem('selectedPlaces', JSON.stringify([id, ...storedIds]));
}
When useEffect is NOT Needed:You don’t need useEffect for:
  • Direct responses to user events (use event handlers)
  • Transforming data for render (compute during render)
  • Simple state updates

Pattern 2: Event Listeners

useEffect(() => {
  const handleResize = () => {
    console.log('Window resized');
  };
  
  window.addEventListener('resize', handleResize);
  
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

Pattern 3: Async Data Fetching

useEffect(() => {
  let ignore = false;
  
  async function fetchData() {
    const response = await fetch('/api/data');
    const data = await response.json();
    
    if (!ignore) {
      setData(data);
    }
  }
  
  fetchData();
  
  return () => {
    ignore = true; // Prevent state update if unmounted
  };
}, []);

Optimizing State Updates

Use functional state updates when new state depends on previous state:
// ✅ Correct - uses previous state
setCounter((prevCounter) => prevCounter + 1);

// ❌ Can be problematic - uses stale closure
setCounter(counter + 1);
Functional Updates Benefits:
  • Always uses the latest state value
  • Prevents bugs from stale closures
  • Required when state update depends on previous value

Common Mistakes

// ❌ Wrong - missing dependency
useEffect(() => {
  console.log(userId);
}, []); // userId not in dependencies

// ✅ Correct
useEffect(() => {
  console.log(userId);
}, [userId]);
React will warn you about this. Always include all dependencies.
// ❌ Wrong - no cleanup
useEffect(() => {
  const timer = setInterval(() => {
    console.log('tick');
  }, 1000);
}, []);

// ✅ Correct - cleanup interval
useEffect(() => {
  const timer = setInterval(() => {
    console.log('tick');
  }, 1000);
  
  return () => clearInterval(timer);
}, []);
// ❌ Wrong - creates infinite loop
useEffect(() => {
  setCount(count + 1); // triggers re-render
}, [count]); // which triggers effect again

// ✅ Correct - runs once or has condition
useEffect(() => {
  if (count < 10) {
    setCount(count + 1);
  }
}, [count]);

Best Practices

Keep Effects Focused

Each effect should handle one concern. Split into multiple effects if needed.

Always Clean Up

Return cleanup functions for timers, subscriptions, and listeners.

List All Dependencies

Include every value used inside the effect in the dependency array.

Use useCallback

Memoize functions passed as dependencies to prevent unnecessary runs.

Resources

React Docs: useEffect

Official documentation for useEffect

You Might Not Need an Effect

Learn when to avoid useEffect

Build docs developers (and LLMs) love