DEV Community

Atilla Baspinar
Atilla Baspinar

Posted on

05 - React: useEffect

This tutorial focuses on one important hook: useEffect.

1) What useEffect does

useEffect runs after React has rendered the component and committed the DOM updates.

That means it is not part of the render itself. It runs after the UI has been painted, which makes it a good place for:

  • fetching data
  • subscribing to events
  • starting timers
  • cleaning up resources

Important timing rule

useEffect runs:

  • once after the initial render
  • again whenever one of its dependencies changes
import { useEffect, useState } from 'react';

export default function TimerExample() {
  const [count, setCount] = useState(0);
  const [userId, setUserId] = useState(1);

  useEffect(() => {
    console.log('Effect runs after render for user:', userId);

    const timer = window.setInterval(() => {
      setCount((prev) => prev + 1);
    }, 1000);

    return () => {
      console.log('Cleanup runs before the next effect or before unmount');
      window.clearInterval(timer);
    };
  }, [userId]);

  return (
    <>
      <p>User: {userId}</p>
      <p>Seconds: {count}</p>
      <button onClick={() => setUserId((prev) => prev + 1)}>Next user</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

What happens in this example?

  1. On the first render, the effect runs after the component is mounted.
  2. It creates a timer and starts counting.
  3. When userId changes, React runs the cleanup function from the previous effect.
  4. Then React runs the new effect for the updated value.
  5. When the component unmounts, the cleanup still runs one final time.

2) Cleanup function

If the effect returns a function, that function is treated as cleanup.

React runs cleanup:

  • before the next effect runs
  • before the component is removed from the DOM

This is important because cleanup is where we usually stop timers, unsubscribe from listeners, and release resources.

useEffect(() => {
  const subscription = someEventSource.subscribe();

  return () => {
    subscription.unsubscribe();
  };
}, [someValue]);
Enter fullscreen mode Exit fullscreen mode

In short:

  • effect = setup work
  • cleanup = teardown work

3) Dependency array

The second argument to useEffect is the dependency array.

useEffect(() => {
  // runs when dependencies change
}, [value1, value2]);
Enter fullscreen mode Exit fullscreen mode
  • [] means run only once after mount
  • [value] means run again whenever value changes
  • no dependency array means it runs after every render

Important: a function in the dependency array can be a problem, especially when that function is passed as a prop.

If the parent component recreates the function on every render, the dependency changes and React will run the effect again.

This is often the reason people use useCallback: to keep a function reference stable unless its real dependencies change.

One useful rule to remember is that state setter functions from useState are stable and do not need to be wrapped in useCallback.

import { useCallback, useEffect, useState } from 'react';

function Child({ onSave }: { onSave: () => void }) {
  useEffect(() => {
    onSave();
  }, [onSave]);

  return <div>Saved</div>;
}

export default function App() {
  const [count, setCount] = useState(0);

  const handleSave = useCallback(() => {
    console.log('saved', count);
  }, [count]);

  return <Child onSave={handleSave} />;
}
Enter fullscreen mode Exit fullscreen mode

In this example, handleSave is recreated only when count changes. That keeps the dependency list honest and avoids unnecessary effect re-runs.

4) Summary

  • useEffect runs after render, not during render
  • it runs at least once after mount
  • it runs again when its dependencies change
  • if it returns a cleanup function, that cleanup runs before the next effect and before unmount

This is the key mental model to remember:

  • render decides what UI should look like
  • effect handles side effects after the render is committed

Top comments (0)