Effects: When Not to Use One

Recognize the most common ways useEffect gets reached for unnecessarily — computing derived values and responding to user actions — and learn to replace those effects with direct computation or event handlers, plus when useLayoutEffect or useInsertionEffect are the right timing instead of useEffect.

useEffect is powerful precisely because it can do almost anything after a render, which is exactly why it gets overused. A huge share of the bugs, extra re-renders, and confusing flicker in real React apps trace back to an effect doing work that should have been either a plain calculation during render or a direct call inside an event handler. This lesson is about recognizing that pattern and removing the unnecessary effect.

The Effect Synchronization Mental Model

The previous lesson framed effects as synchronization with something outside React — a DOM API, a network connection, a subscription. The corollary is important: if a piece of logic doesn't touch anything outside the component, it isn't synchronizing with anything, and it doesn't belong in an effect. Two categories account for most of these misplaced effects: computing a value from existing props or state, and responding to something the user did.

The underlying reason both are problems is the same. An effect only runs after a render has already committed, so using one to compute a value or react to a click means the component renders once with stale or missing data, then re-renders a moment later with the corrected value — an extra render cycle, and often a visible flicker, that a direct computation or handler call would have skipped entirely.

Derived State: Compute During Render Instead

A very common anti-pattern is storing a value in its own state variable and using an effect to keep it 'in sync' with some other state, when the value could simply be computed directly in the component body on every render — no effect, no extra state, no extra render required.

Before: fullName is held in its own state, and an effect recalculates it whenever firstName or lastName changes. This works, but it means every name change causes two renders — one with the stale fullName, then a second one after the effect runs and updates it — and it adds a dependency array that has to be kept correct by hand.

After: fullName is just ${firstName} ${lastName}, computed inline during render. There's no effect, no extra state to keep in sync, no possibility of the two falling out of sync, and no extra render — the correct value is available the very first time the component renders with the new names.

The general rule: if a value can be calculated from current props and state, calculate it during render. Reach for useMemo only if that calculation is measurably expensive — the plain calculation, with no memoization at all, is the correct default.

User Actions Belong in Event Handlers, Not Effects

The second common anti-pattern is using an effect to watch for a change caused by a user action and then run some logic in response, instead of just running that logic directly in the event handler that caused the change in the first place.

Before: clicking 'Submit' sets a submitted flag to true, and a separate effect watches that flag, and when it becomes true, it calls an API and shows a toast notification. This buries the actual cause-and-effect relationship behind an indirect state flag, adds an unnecessary re-render between the click and the resulting action, and makes it easy to accidentally re-trigger the effect (for example, if the flag gets reset and set again for an unrelated reason).

After: the onClick handler calls the API and shows the toast directly, with no separate flag and no effect at all. The code reads in the order things actually happen — click, then submit, then notify — and there's no risk of the 'submission' logic firing for a reason other than an actual click.

The test to apply: was this code caused by a specific user interaction (a click, a form submission, a key press)? If so, it belongs in that interaction's event handler. Was this code needed to keep the component synchronized with something external regardless of why the relevant value changed? If so, and only then, it belongs in an effect.

useLayoutEffect vs useEffect vs useInsertionEffect

All three hooks run after React commits changes to the DOM, but they differ in exactly when relative to the browser painting that update to the screen. useEffect runs asynchronously after the browser has already painted, which is why it's the default choice — it never blocks the user from seeing the update.

useLayoutEffect runs synchronously immediately after the DOM is updated but before the browser paints. Use it specifically when an effect needs to measure something in the DOM (like an element's size or position) and then synchronously make another DOM change before the user sees anything — for example, positioning a tooltip based on measuring its own rendered size. Using useEffect for that same measurement would let the browser paint the tooltip in the wrong position first, then visibly snap it into the right place a frame later.

useInsertionEffect runs even earlier than useLayoutEffect, before any DOM mutations from layout effects are applied, and it exists almost exclusively for CSS-in-JS libraries that need to inject <style> tags into the document before layout effects run and start measuring elements whose styles depend on those tags. Application code essentially never needs useInsertionEffect directly — it's infrastructure for library authors, not a general-purpose hook.

Removing an unnecessary derived-state effect

The 'after' version has no effect, no dependency array to maintain, and no extra render — fullName is correct on the very first render that has the new names.

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

// After: computed directly during render
const fullName = `${firstName} ${lastName}`;

Code Examples

This renders once with the previous filtered list (or the unfiltered products, on first render), then re-renders a moment later once the effect runs and updates filtered — a visible flicker and an unnecessary extra render on every keystroke of the search query.

Common mistake: Every change to query causes two renders instead of one, and filtered can briefly be out of sync with the actual current query.

filtered is recalculated on every render directly from the current products and query — always correct, no extra state, no effect, and no possibility of a stale intermediate render. If this filter became expensive on a huge list, useMemo would be the next step, not useEffect.

const filtered = products.filter(p => p.name.includes(query));

Because useLayoutEffect runs before the browser paints, the tooltip is repositioned based on its measured height before the user ever sees it in the wrong place — using useEffect here would let the incorrect position flash on screen for one frame first.

function ProductList({ products, query }: { products: Product[]; query: string }) {
  const [filtered, setFiltered] = useState<Product[]>(products);

  useEffect(() => {
    setFiltered(products.filter(p => p.name.includes(query)));
  }, [products, query]);

  return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
function ProductList({ products, query }: { products: Product[]; query: string }) {
  const filtered = products.filter(p => p.name.includes(query));

  return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
function Tooltip({ children }: { children: React.ReactNode }) {
  const ref = useRef<HTMLDivElement>(null);
  const [top, setTop] = useState(0);

  useLayoutEffect(() => {
    const height = ref.current?.getBoundingClientRect().height ?? 0;
    setTop(-height - 8); // position above the trigger, before paint
  }, []);

  return <div ref={ref} style={{ position: "absolute", top }}>{children}</div>;
}

Important Rules

If a value can be computed from current props and state, compute it directly during render — don't store it in its own state kept 'in sync' by an effect.

If logic is a direct response to a specific user action, put it in that action's event handler, not in an effect that watches for the resulting state change.

Default to useEffect; reach for useLayoutEffect only when you must measure the DOM and synchronously adjust it before the browser paints.

Treat useInsertionEffect as library-author infrastructure for injecting styles — application code almost never needs to call it directly.

Where this shows up

Search-as-you-type filtering, computed totals in a shopping cart, and formatted display strings derived from raw state are all places production codebases frequently — and unnecessarily — reach for an effect plus extra state when a direct calculation during render would be simpler, faster, and less buggy. Tooltip, popover, and dropdown positioning libraries are the canonical legitimate use of useLayoutEffect, since they must measure rendered content and adjust position before the user sees anything.

const [total, setTotal] = useState(0); useEffect(() => { setTotal(items.reduce((sum, i) => sum + i.price, 0)); }, [items]);

const total = items.reduce((sum, i) => sum + i.price, 0);

total is fully derivable from items on every render, so storing it in state and syncing it with an effect only adds an extra render cycle and a chance for total to be stale for one render after items changes.

[concept] A value can be fully computed from the component's current props and state. Where should that computation live? A. In a useEffect with the relevant props/state as dependencies, storing the result in its own state B. Directly in the component body during render, with no extra state or effect C. In a useLayoutEffect for guaranteed synchronous timing D. In useInsertionEffect so it runs before other effects

B. Directly in the component body during render, with no extra state or effect — Anything derivable from current props and state should be computed directly during render. Using an effect for this adds an unnecessary extra render and a dependency array to maintain for no benefit.

[concept] Clicking a 'Save' button should call an API and show a success toast. What's the correct place for that logic? A. Directly inside the button's onClick handler B. In a useEffect that watches a 'saveClicked' state flag set by onClick C. In useLayoutEffect so it runs before paint D. In useInsertionEffect

A. Directly inside the button's onClick handler — This logic is a direct response to a specific user action, so it belongs in that action's event handler. Routing it through an effect watching a flag adds an indirection and an extra render for no benefit.

[concept] When is useLayoutEffect the right choice instead of useEffect? A. Whenever an effect reads props or state B. Whenever fetching data from an API C. When you must measure the DOM and synchronously adjust it before the browser paints, to avoid a visible flicker D. Whenever a component uses more than one dependency in its effect

C. When you must measure the DOM and synchronously adjust it before the browser paints, to avoid a visible flicker — useLayoutEffect runs synchronously after DOM mutations but before paint, which is specifically useful for measure-then-adjust scenarios like positioning a tooltip. useEffect, which runs after paint, would let the wrong position flash on screen first.

A component stores an isValid boolean in state and uses an effect that watches email and password to recompute it. Refactor it to remove the effect and the extra state.

function SignupForm({ email, password }: { email: string; password: string }) { const isValid = email.includes("@") && password.length >= 8; // no useState, no useEffect needed for isValid return <button disabled={!isValid}>Sign up</button>; } isValid is fully derivable from email and password on every render — no effect is needed at all.