Skip to main content

`useEffect` is not your first option. It is your last resort.

Every React dev learns useEffect early and then spends years overusing it.

It is not your fault. The name sounds generic enough to fit anything. But the API has a specific contract: synchronize with the world outside React. DOM, network, browser APIs.

If you are not crossing that boundary, you probably do not need an effect.


Derived state does not need an effect

The most common mistake. One value depends on another piece of state, so you sync them:

// Don't do thisconst [items, setItems] = useState([]);const [count, setCount] = useState(0); useEffect(() => {  setCount(items.length);}, [items]);

Two renders for something you can compute directly. React renders with stale count, the effect runs, updates state, then React renders again.

Just compute it:

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

No extra state. No effect. No extra render.


Filtering data does not need an effect

Same mistake, different shape:

// Don't do thisconst [query, setQuery] = useState("");const [filtered, setFiltered] = useState(todos); useEffect(() => {  setFiltered(todos.filter((t) => t.text.includes(query)));}, [todos, query]);

Two renders per change. Compute inline:

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

If the computation is heavy, use useMemo. It caches the result instead of scheduling state updates like useEffect + setState:

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

Responding to events does not need an effect

User did something? Handle it in the event handler:

// Don't do thisconst [submitted, setSubmitted] = useState(false); useEffect(() => {  if (submitted) {    sendAnalytics("form_submit");    setSubmitted(false);  }}, [submitted]); function handleSubmit() {  setSubmitted(true);}

This is an indirect way of saying "when the user submits, fire analytics". Put it directly in the handler:

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

Simple rule: user action -> handler. Component appeared and must sync with something external -> effect.


Resetting state when a prop changes does not need an effect

// Don't do thisfunction ProfilePage({ userId }) {  const [comment, setComment] = useState("");   useEffect(() => {    setComment("");  }, [userId]);   // ...}

It first renders stale state, then the effect clears it. Use key:

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

Different key means a new instance. State starts clean with no extra code.


When useEffect is the right answer

  • Event listeners (resize, scroll, WebSockets)
  • Integrations with libraries that manipulate the DOM directly
  • Data fetching, but prefer SWR or TanStack Query because they handle race conditions you will likely miss
// This is a valid use of useEffectuseEffect(() => {  const handler = () => setWindowWidth(window.innerWidth);  window.addEventListener("resize", handler);  return () => window.removeEventListener("resize", handler);}, []);

The question that solves everything

Before writing useEffect:

Am I synchronizing with something outside React?

  • Computing a value from state or props? Compute during render.
  • Heavy computation? Use useMemo.
  • Responding to user action? Use a handler.
  • Need to reset state when a prop changes? Use key.

useEffect is an escape hatch, not the default.

Go Back