Skip to content
Back to blog

You might not need useEffect: 5 patterns to delete

useEffect is the hook people reach for when they do not know which hook to use. It runs, it works, and the bug shows up a week later as a flicker, a stale value or a request that fires twice. Reviewing React code, I delete more effects than I add.

The test I use is one question: does this code run because the component was shown, or because something happened? Only the first one belongs in an effect. Here are five patterns that usually fail that test.

What an effect is for

An effect synchronizes your component with something outside React: a WebSocket, a timer, a browser API, a non-React widget. It runs after render, because it needs the DOM and the result of that render to exist.

If there is no outside system involved, you are probably moving data around inside React, and that is better done during render or in an event handler.

1. Derived state

The most common one: copying props or state into more state, then keeping it in sync with an effect.

Broken
function Cart({ items }: { items: { price: number }[] }) {
  const [total, setTotal] = useState(0);

  useEffect(() => { 
    setTotal(items.reduce((sum, item) => sum + item.price, 0)); 
  }, [items]); 

  return <p>Total: {total}</p>;
}

This renders once with the old total, runs the effect, then renders again with the new one. Two renders, and a frame where the number is wrong.

Fixed
function Cart({ items }: { items: { price: number }[] }) {
  const total = items.reduce((sum, item) => sum + item.price, 0); 

  return <p>Total: {total}</p>;
}

If the calculation is genuinely expensive, wrap it in useMemo. But measure first, as I wrote in the memoization post.

2. Resetting state when a prop changes

Another classic: a profile page that must clear a draft when the user id changes.

Broken
function Profile({ userId }: { userId: string }) {
  const [comment, setComment] = useState("");

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

  /* ... */
}

React already has a tool for “this is a different thing now”: the key prop. Changing the key throws away the component and its state, then mounts a fresh one.

Fixed
function ProfilePage({ userId }: { userId: string }) {
  return <Profile key={userId} userId={userId} />; 
}

No effect, no stale frame, and every piece of state inside Profile resets, including the ones you forgot about.

3. Work that belongs in an event handler

If something should happen because the user clicked, put it in the click handler. An effect that watches state to decide what to do next is an event handler wearing a disguise.

Broken
function Checkout() {
  const [submitted, setSubmitted] = useState(false);

  useEffect(() => { 
    if (submitted) { 
      track("checkout_submitted"); 
      showToast("Order placed"); 
    } 
  }, [submitted]); 

  return <button onClick={() => setSubmitted(true)}>Pay</button>;
}
Fixed
function Checkout() {
  const handlePay = () => { 
    track("checkout_submitted"); 
    showToast("Order placed"); 
  }; 

  return <button onClick={handlePay}>Pay</button>;
}

The handler knows exactly why it ran. The effect only knows that some state changed, which is how you end up with a toast that shows again after a hot reload.

4. Fetching data in an effect

Fetching in useEffect looks easy, and the easy version has a race condition. If query changes quickly, a slow earlier response can arrive after a fast later one and overwrite it.

Fragile
useEffect(() => {
  let ignore = false; 

  fetch(`/api/search?q=${query}`)
    .then((res) => res.json())
    .then((data) => {
      if (!ignore) setResults(data); 
    });

  return () => { 
    ignore = true; 
  }; 
}, [query]);

The ignore flag fixes the race, but you still have no caching, no loading and error handling, and no deduplication. In a real app I would use a data library such as TanStack Query, or fetch on the server in a framework that supports it. Both remove the effect entirely.

With TanStack Query
const { data, isPending } = useQuery({
  queryKey: ["search", query],
  queryFn: () => fetch(`/api/search?q=${query}`).then((res) => res.json()),
});

5. Notifying a parent after state changes

Calling a parent callback from an effect adds an extra render pass and an extra place for bugs.

Broken
function Toggle({ onChange }: { onChange: (on: boolean) => void }) {
  const [on, setOn] = useState(false);

  useEffect(() => { 
    onChange(on); 
  }, [on, onChange]); 

  return <button onClick={() => setOn(!on)}>{String(on)}</button>;
}

Update both in the same event instead, so React batches them into one render.

Fixed
function Toggle({ onChange }: { onChange: (on: boolean) => void }) {
  const [on, setOn] = useState(false);

  const toggle = () => { 
    const next = !on; 
    setOn(next); 
    onChange(next); 
  }; 

  return <button onClick={toggle}>{String(on)}</button>;
}

If the parent needs to control the value, lift the state up and make Toggle a controlled component. Then there is only one source of truth.

Do I need an effect?
  1. Can I compute it in render?derived state
  2. Did the user cause it?event handler
  3. Is it a different thing?use a key
  4. Is it server data?query library or server
  5. Syncing with the outside worldnow useEffect is right

Walk down the list. Stop at the first yes.

When an effect is the right tool

Real effects do exist. Subscribing to a store or a WebSocket, setting up a ResizeObserver, wiring a third-party chart library, focusing an element after it mounts. All of them touch something React does not own, and all of them need a cleanup function.

useWindowWidth.ts
import { useEffect, useState } from "react";

export function useWindowWidth() {
  const [width, setWidth] = useState(0);

  useEffect(() => {
    const update = () => setWidth(window.innerWidth);
    update();
    window.addEventListener("resize", update); 
    return () => window.removeEventListener("resize", update); 
  }, []);

  return width;
}

For subscribing to an external store specifically, useSyncExternalStore is better still, because it avoids tearing during concurrent rendering. Wrap an effect in a small custom hook like the one above, and components that use it never see an effect.

Interview answer in 30 seconds

“Effects synchronize a component with an external system. If I can compute a value from props and state, I do it during render. If a user action caused it, it goes in the event handler. To reset state when an id changes, I use a key. For server data I use a query library or the server, and I always clean up subscriptions. If none of those apply, then useEffect is the right tool.”

Key takeaways

  • An effect is for syncing with something outside React, not for moving data inside it.
  • Compute derived values during render. Do not mirror them in state.
  • Use key to reset state when an identity changes.
  • Put user-caused work in event handlers, not in effects that watch state.
  • Fetching in an effect needs race protection. A query library handles it.
  • Real effects need a cleanup function.