useEffect runs after the first render and after every update. But plenty of effects only make sense on changes — refetching when a filter changes, saving a draft when it edits, reacting to a prop that moved — and firing them on mount duplicates work you already did during initial render, or worse, triggers a spurious network call or toast the instant the component appears. useUpdateEffect is useEffect with the mount run skipped: it fires only on subsequent dependency changes.
Implement useUpdateEffect(effect, deps). Same signature as useEffect. Skip the very first run; on every later run caused by a dependency change, invoke effect and honor any cleanup function it returns.
function useUpdateEffect(effect, deps) {
// like useEffect, but does NOT run on mount
}
useUpdateEffect(() => {
refetch(query); // runs when `query` changes, NOT on mount
}, [query]);
useUpdateEffect(() => {
const id = setInterval(tick, delay);
return () => clearInterval(id); // cleanup honored, just like useEffect
}, [delay]);
effect.useEffect for all subsequent dependency changes.effect returns so React can clean up before the next run and on unmount.You'll wrap useEffect with a ref that swallows the first run, so effect fires only on dependency changes after mount — cleanup and all.
useEffect can't be told "not on mount" — it always runs once after the initial render. When your effect is a reaction to change (refetch on new query, autosave on edit), that mount run is a false trigger: the data's already fresh, the draft's already saved. You need a one-render memory that says "I've already mounted," checked inside the effect so the first invocation bails out and later ones proceed normally.
It's a turnstile that lets the first person through without counting them. The effect still runs after mount — React insists — but inside, a ref flag intercepts that first run, flips itself, and returns early before effect is called. From the second run onward the flag is down, so the effect behaves like a plain useEffect: run effect, register its cleanup, clean up before the next run.
The naive version checks a plain boolean declared in the hook body:
function useUpdateEffectNaive(effect, deps) {
let isFirst = true; // reset to true on EVERY render
useEffect(() => {
if (isFirst) {
isFirst = false;
return;
}
return effect();
}, deps);
}
isFirst is a local variable, so it's re-initialized to true on every render. The effect's closure captures whatever isFirst was for that render — always true at the moment the effect runs — so the effect thinks every run is the first and never calls effect. The flag has to persist across renders, which means a ref, not a local.
const { useEffect, useRef } = require('react');
function useUpdateEffect(effect, deps) {
const isFirst = useRef(true);
useEffect(() => {
if (isFirst.current) {
isFirst.current = false; // swallow the mount run
return; // no effect, no cleanup registered
}
return effect(); // subsequent runs behave like useEffect
}, deps);
}
module.exports = { useUpdateEffect };
The key is useRef(true): its .current persists across renders, so the flag survives. On the mount run the guard is true — we set it false and return immediately, registering no cleanup. Every later run (a dependency changed) finds .current already false and falls through to return effect(), forwarding the effect's cleanup to React so it runs before the next effect and on unmount. Because we pass deps straight through to useEffect, change detection is React's, unchanged.
Mount with deps = [0], then re-render with [1], then [2]:
[0]) — the effect runs; isFirst.current is true, so it flips to false and returns. effect is not called; no cleanup registered.[1] — dep changed, effect runs; isFirst.current is false, so effect() runs. If it returns a cleanup, React stores it.[2] — before running, React calls the stored cleanup from the [1] run; then effect() runs again for [2].[2] run. The mount run contributed no cleanup, so nothing extra fires.The effect fired for [1] and [2] — the changes — and never for the mount.
true and never fires. Use useRef.return effect() (not just effect()) so the effect's cleanup reaches React.deps; keep the dependency array honest at the call site.useUpdateLayoutEffect — the same skip-mount trick over useLayoutEffect for synchronous, pre-paint reactions.useIsFirstRender — this hook is that one applied to an effect; factoring the ref logic out makes both read cleanly.deps yields "run only when this object value changes," useful when deps are freshly-built objects each render.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
useEffect runs after the first render and after every update. But plenty of effects only make sense on changes — refetching when a filter changes, saving a draft when it edits, reacting to a prop that moved — and firing them on mount duplicates work you already did during initial render, or worse, triggers a spurious network call or toast the instant the component appears. useUpdateEffect is useEffect with the mount run skipped: it fires only on subsequent dependency changes.
Implement useUpdateEffect(effect, deps). Same signature as useEffect. Skip the very first run; on every later run caused by a dependency change, invoke effect and honor any cleanup function it returns.
function useUpdateEffect(effect, deps) {
// like useEffect, but does NOT run on mount
}
useUpdateEffect(() => {
refetch(query); // runs when `query` changes, NOT on mount
}, [query]);
useUpdateEffect(() => {
const id = setInterval(tick, delay);
return () => clearInterval(id); // cleanup honored, just like useEffect
}, [delay]);
effect.useEffect for all subsequent dependency changes.effect returns so React can clean up before the next run and on unmount.You'll wrap useEffect with a ref that swallows the first run, so effect fires only on dependency changes after mount — cleanup and all.
useEffect can't be told "not on mount" — it always runs once after the initial render. When your effect is a reaction to change (refetch on new query, autosave on edit), that mount run is a false trigger: the data's already fresh, the draft's already saved. You need a one-render memory that says "I've already mounted," checked inside the effect so the first invocation bails out and later ones proceed normally.
It's a turnstile that lets the first person through without counting them. The effect still runs after mount — React insists — but inside, a ref flag intercepts that first run, flips itself, and returns early before effect is called. From the second run onward the flag is down, so the effect behaves like a plain useEffect: run effect, register its cleanup, clean up before the next run.
The naive version checks a plain boolean declared in the hook body:
function useUpdateEffectNaive(effect, deps) {
let isFirst = true; // reset to true on EVERY render
useEffect(() => {
if (isFirst) {
isFirst = false;
return;
}
return effect();
}, deps);
}
isFirst is a local variable, so it's re-initialized to true on every render. The effect's closure captures whatever isFirst was for that render — always true at the moment the effect runs — so the effect thinks every run is the first and never calls effect. The flag has to persist across renders, which means a ref, not a local.
const { useEffect, useRef } = require('react');
function useUpdateEffect(effect, deps) {
const isFirst = useRef(true);
useEffect(() => {
if (isFirst.current) {
isFirst.current = false; // swallow the mount run
return; // no effect, no cleanup registered
}
return effect(); // subsequent runs behave like useEffect
}, deps);
}
module.exports = { useUpdateEffect };
The key is useRef(true): its .current persists across renders, so the flag survives. On the mount run the guard is true — we set it false and return immediately, registering no cleanup. Every later run (a dependency changed) finds .current already false and falls through to return effect(), forwarding the effect's cleanup to React so it runs before the next effect and on unmount. Because we pass deps straight through to useEffect, change detection is React's, unchanged.
Mount with deps = [0], then re-render with [1], then [2]:
[0]) — the effect runs; isFirst.current is true, so it flips to false and returns. effect is not called; no cleanup registered.[1] — dep changed, effect runs; isFirst.current is false, so effect() runs. If it returns a cleanup, React stores it.[2] — before running, React calls the stored cleanup from the [1] run; then effect() runs again for [2].[2] run. The mount run contributed no cleanup, so nothing extra fires.The effect fired for [1] and [2] — the changes — and never for the mount.
true and never fires. Use useRef.return effect() (not just effect()) so the effect's cleanup reaches React.deps; keep the dependency array honest at the call site.useUpdateLayoutEffect — the same skip-mount trick over useLayoutEffect for synchronous, pre-paint reactions.useIsFirstRender — this hook is that one applied to an effect; factoring the ref logic out makes both read cleanly.deps yields "run only when this object value changes," useful when deps are freshly-built objects each render.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.