back

You don't need useEffect

If you've been writing React for a while, you've probably built a habit of reaching for useEffect whenever something needs to "happen." Data needs transforming? useEffect. A value depends on props? useEffect. User clicks a button and something should update? Also useEffect.

Most of these cases don't need an effect. Effects are for syncing with external systems like the DOM, a network request, or a browser API. If you're using them to derive state or respond to user events, you're adding complexity and extra re-renders for no reason.

Derived state doesn't need an effect

This is the most common misuse. You have state that depends on other state or props, so you sync them with an effect:

// Don't do this
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);

useEffect(() => {
  setCount(items.length);
}, [items]);

This triggers an extra render every time items changes. React renders with the stale count, then the effect runs, updates count, and React renders again. Two renders for something you can compute directly.

Just calculate it:

// Do this instead
const [items, setItems] = useState([]);
const count = items.length;

No state. No effect. No extra render.

Filtering data doesn't need an effect

Same mistake, different shape. Transforming a list based on some filter:

// Don't do this
const [query, setQuery] = useState("");
const [filtered, setFiltered] = useState(todos);

useEffect(() => {
  setFiltered(todos.filter((t) => t.text.includes(query)));
}, [todos, query]);

Two renders per change. Just compute it inline:

// Do this instead
const [query, setQuery] = useState("");
const filtered = todos.filter((t) => t.text.includes(query));

If the filtering is expensive, wrap it in useMemo:

const filtered = useMemo(
  () => todos.filter((t) => t.text.includes(query)),
  [todos, query]
);

useMemo caches the result. It doesn't schedule a state update like useEffect + setState does.

Responding to events doesn't need an effect

When a user does something and you want to react to it, put that logic in the event handler, not an effect:

// Don't do this
const [submitted, setSubmitted] = useState(false);

useEffect(() => {
  if (submitted) {
    sendAnalytics("form_submit");
    setSubmitted(false);
  }
}, [submitted]);

function handleSubmit() {
  setSubmitted(true);
}

This is a roundabout way of saying "when the user submits, send analytics." Just put it in the handler:

// Do this instead
function handleSubmit() {
  sendAnalytics("form_submit");
}

If something happens because of a user action, it goes in the event handler. If something happens because the component appeared on screen and you need to sync with something external, that's an effect.

Resetting state on prop change doesn't need an effect

A common pattern when a component receives a new ID and needs to reset its internal state:

// Don't do this
function ProfilePage({ userId }) {
  const [comment, setComment] = useState("");

  useEffect(() => {
    setComment("");
  }, [userId]);

  // ...
}

This renders with stale state first, then the effect clears it. Use a key instead to tell React this is a different component instance:

// Do this instead
// In the parent:
<ProfilePage userId={id} key={id} />

When the key changes, React unmounts the old instance and mounts a fresh one. State starts clean.

When you actually need useEffect

Effects are the right tool when you're syncing with something outside React:

// This is a valid use of useEffect
useEffect(() => {
  const handler = (e) => setWindowWidth(window.innerWidth);
  window.addEventListener("resize", handler);
  return () => window.removeEventListener("resize", handler);
}, []);

The mental model

Before writing useEffect, ask: "Am I syncing with something outside of React?" If no, you probably don't need it.

Effects are an escape hatch, not a default.