mosaddik.dev
All posts

React useEffect explained, and when you don't need it

Mosaddik Billah 4 min read

JavaScript & React

The React logo next to code on a screen
Photo by Lautaro Andreani on Unsplash
On this page
  1. The basic shape
  2. The dependency array
  3. Cleanup
  4. Fetching data without race conditions
  5. Why does my effect run twice?
  6. When you don’t need an effect
  7. Summary

useEffect is the React hook people reach for most and misunderstand most. The short version: an effect keeps your component in sync with something outside React, such as a network request, a timer, a browser API or a third-party widget. If nothing outside React is involved, you probably don’t need one.

The basic shape#

JSX
import { useEffect } from "react";

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

  return <h1>{title}</h1>;
}

React first renders the component and updates the screen, then runs the effect. Here the effect copies title into the browser tab, which lives outside React.

The dependency array#

The second argument tells React when to run the effect again:

JSX
useEffect(() => { /* ... */ });         // after every render
useEffect(() => { /* ... */ }, []);     // once, after the first render
useEffect(() => { /* ... */ }, [id]);   // after the first render, and whenever id changes

List every value from the component that the effect uses: props, state and functions defined in the component. If you leave one out, the effect keeps using an old value. The react-hooks/exhaustive-deps lint rule catches this for you, so keep it turned on.

Cleanup#

An effect can return a function. React calls it before running the effect again, and when the component is removed:

JSX
function Clock() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const timer = setInterval(() => setTime(new Date()), 1000);
    return () => clearInterval(timer);
  }, []);

  return <p>{time.toLocaleTimeString()}</p>;
}

Without the cleanup, every time Clock appeared on screen it would start another timer that never stopped. Anything you start in an effect, such as a timer, a subscription or an event listener, should be stopped in its cleanup.

Fetching data without race conditions#

A common bug: the user switches from product 1 to product 2, but product 1’s slower response arrives last and overwrites the screen. Cleanup fixes it:

JSX
function Product({ id }) {
  const [product, setProduct] = useState(null);
  const [error, setError] = useState(null);

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

    fetch(`/api/products/${id}`, { signal: controller.signal })
      .then((response) => {
        if (!response.ok) throw new Error(`Request failed: ${response.status}`);
        return response.json();
      })
      .then(setProduct)
      .catch((err) => {
        if (err.name !== "AbortError") setError(err);
      });

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

  if (error) return <p>Couldn’t load this product.</p>;
  if (!product) return <p>Loading…</p>;
  return <h2>{product.name}</h2>;
}

When id changes, the cleanup cancels the old request before the new one starts, so only the latest response is used. In a larger app, a data library such as TanStack Query, or your framework’s data loading, handles this along with caching and retries.

Why does my effect run twice?#

In development, React’s Strict Mode mounts each component, removes it, and mounts it again, so effects run, clean up, and run again. This is deliberate: it reveals effects that are missing a cleanup. It doesn’t happen in production. If running twice breaks something, fix the cleanup rather than removing Strict Mode.

When you don’t need an effect#

Many effects only copy data from one place in React to another. They add an extra render and a chance for the two to get out of sync.

Calculating values from props or state. Calculate them during render:

JSX
// Unnecessary effect and extra state
const [fullName, setFullName] = useState("");
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// Simpler
const fullName = `${firstName} ${lastName}`;

If the calculation is slow, wrap it in useMemo instead of an effect.

Responding to a click or submit. Put the code in the event handler. An effect runs because something rendered; a handler runs because the user did something, which is usually what you mean:

JSX
// Do this in the handler, not in an effect that watches "submitted"
async function handleSubmit(event) {
  event.preventDefault();
  await saveForm(values);
  showToast("Saved");
}

A good test: if you can describe the code as “when the user does X”, it belongs in a handler. If it’s “while this component is on screen, stay in sync with Y”, it’s an effect.

Summary#

  • Use an effect to sync with something outside React.
  • List everything the effect uses in the dependency array.
  • Undo in the cleanup whatever the effect starts.
  • Calculate derived values during render, and put user actions in event handlers.

React is also what powers dashboard pages and extensions in Wix apps. If you need a custom Wix app built, I do Wix apps with the Wix CLI.

Comments