Skip to main content
useEffect runs side effects after the component renders. Effects are scheduled to run after browser paint, without blocking the UI.

Signature

function useEffect(
  effect: () => void | (() => void),
  inputs?: ReadonlyArray<unknown>
): void

Parameters

effect
() => void | (() => void)
required
A function containing the imperative, effectful code. Can optionally return a cleanup function that will be called before the effect runs again or when the component unmounts.
inputs
ReadonlyArray<unknown>
An array of dependencies. The effect will only re-run if one of the values in this array has changed (compared using ===). If omitted, the effect runs after every render.

Returns

void

Basic Usage

import { useEffect, useState } from 'preact/hooks';

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

  useEffect(() => {
    // Update document title after render
    document.title = `Count: ${count}`;
  }, [count]); // Only re-run when count changes

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

Effect with Cleanup

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

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // Cleanup function
    return () => {
      clearInterval(interval);
    };
  }, []); // Empty array = run once on mount

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

Fetching Data

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

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

    async function fetchUser() {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        if (!cancelled) {
          setUser(data);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    fetchUser();

    // Cleanup: prevent state updates if component unmounts
    return () => {
      cancelled = true;
    };
  }, [userId]); // Re-fetch when userId changes

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

Event Listeners

function WindowSize() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => {
      setWidth(window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    // Cleanup: remove listener on unmount
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Empty deps = only setup/cleanup once

  return <div>Window width: {width}px</div>;
}

Run Once on Mount

function InitialSetup() {
  useEffect(() => {
    console.log('Component mounted');

    return () => {
      console.log('Component will unmount');
    };
  }, []); // Empty dependency array

  return <div>Hello</div>;
}

Multiple Effects

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

  // Effect for count
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  // Effect for name
  useEffect(() => {
    localStorage.setItem('name', name);
  }, [name]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <input value={name} onChange={e => setName(e.target.value)} />
    </div>
  );
}
Effects run after the browser has painted, making them suitable for most side effects. For effects that must run synchronously before paint (like measuring DOM layout), use useLayoutEffect instead.
Always include all values from the component scope that are used inside the effect in the dependency array. This ensures your effect always has the latest values.
If your effect returns a cleanup function, it will be called before running the effect again (when dependencies change) and when the component unmounts.

Build docs developers (and LLMs) love