Build a custom hook that runs a side effect a single time, right after the component mounts, and never again — no matter how many times the component re-renders afterward. React's useEffect re-runs whenever one of its dependencies changes; with an empty dependency list it runs only on mount. useEffectOnce(effect) packages that pattern behind a name, and runs the effect's cleanup function (if it returns one) when the component unmounts.
function useEffectOnce(effect: () => (void | (() => void))): void;
The hook returns nothing. effect is the function to run once; if it returns a function, that returned function is the cleanup that runs on unmount.
function Chat({ roomId }) {
// Runs ONCE after this component first appears, even if `roomId` or other
// props change and trigger re-renders. The returned function tears down
// the connection when the component unmounts.
useEffectOnce(() => {
const conn = openConnection();
return () => conn.close();
});
return <div>Connected</div>;
}
// Across the component's life, the effect fires exactly one time:
// mount → effect runs (call count: 1)
// re-render → effect skipped (call count: 1)
// re-render → effect skipped (call count: 1)
// unmount → cleanup runs (cleanup count: 1)
effect returns a function, run it on unmount. If it returns nothing, unmount must not error.StrictMode, effects are not double-invoked, so the effect runs exactly once per mount.You'll wrap useEffect with an intentionally empty dependency array so the effect runs a single time after mount and its cleanup runs on unmount.
Some setup should happen exactly once when a component appears: open a socket, start a subscription, fire an analytics "screen viewed" event, kick off a one-time fetch. The catch is that components re-render constantly — a parent updates, a piece of state changes, a prop arrives — and you do not want that setup running again on every one of those renders. useEffectOnce gives you a single, named place to run that mount-time work once, and to tear it down when the component goes away.
A component's life is one mount, then any number of re-renders, then one unmount. You want your effect to attach to just the first of those events and your cleanup to attach to the last, ignoring everything in between. React already has a lever for this: an effect's dependency array tells React when to re-run the effect. List nothing, and there is nothing that can ever change, so React runs the effect on mount and the cleanup on unmount — and skips every render in between.
The obvious move is to just hand the effect straight to useEffect:
const { useEffect } = require('react');
function useEffectOnce(effect) {
useEffect(effect);
}
module.exports = { useEffectOnce };
This runs the effect after the first render — so far so good — but it also runs it after every render. With no dependency array, useEffect treats the effect as "depends on everything," so React re-runs it on each commit (and runs the cleanup before each re-run). A component that re-renders five times calls your "once" effect five times. The shape is right; the trigger is wrong.
const { useEffect } = require('react');
function useEffectOnce(effect) {
// The empty dependency array is the whole point: it tells React "this effect
// depends on nothing, so there is never a reason to re-run it." React runs it
// after the mount render and runs its cleanup (if any) on unmount — and skips
// every render in between.
useEffect(effect, []);
}
module.exports = { useEffectOnce };
The only change from the naive version is the second argument: []. That empty array is a promise to React that the effect reads nothing that can change between renders, so React has no reason to ever run it a second time. Whatever effect returns becomes the cleanup React stores and invokes on unmount; if it returns nothing, there is simply no cleanup to run.
Picture a Chat component whose effect opens a connection and returns () => conn.close():
Chat, commits it to the screen, then runs the effect once. The connection opens. React stores the returned () => conn.close() as this effect's cleanup.Chat re-renders. React checks the dependency array, sees [] — identical to last time, nothing changed — and skips the effect entirely. The connection stays open; no second connection is made.Chat is removed. React calls the stored cleanup once: conn.close(). The connection is torn down.Across the whole life of the component the effect ran once and the cleanup ran once, exactly as intended.
useEffect(effect) with no second argument runs on every render, not once. Fix: pass [] so the effect is tied to mount.if (!ran.current) { ran.current = true; effect(); } directly in the render body runs the effect during render rather than after commit, and never registers a cleanup for unmount. Fix: use useEffect(effect, []) so React handles both timing and teardown.effect returns nothing, there is no cleanup — unmount must not assume one exists. Passing the effect straight to useEffect handles this for free; React only calls a cleanup if the effect returned a function.useEffect with that value in its dependency array, not useEffectOnce.react-hooks/exhaustive-deps lint rule flags an empty array when the effect closes over props or state, because that effect will read stale values. useEffectOnce opts out of that check by design — document the intent so readers know the staleness is deliberate.useMount / useUnmount pair. Some libraries split this into two hooks — one for mount-only setup, one for unmount-only teardown — built on the same empty-array trick.StrictMode, React intentionally mounts, unmounts, and remounts components to surface missing cleanup, so the effect runs twice. That is a dev-only check; production and non-StrictMode trees run it once.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Build a custom hook that runs a side effect a single time, right after the component mounts, and never again — no matter how many times the component re-renders afterward. React's useEffect re-runs whenever one of its dependencies changes; with an empty dependency list it runs only on mount. useEffectOnce(effect) packages that pattern behind a name, and runs the effect's cleanup function (if it returns one) when the component unmounts.
function useEffectOnce(effect: () => (void | (() => void))): void;
The hook returns nothing. effect is the function to run once; if it returns a function, that returned function is the cleanup that runs on unmount.
function Chat({ roomId }) {
// Runs ONCE after this component first appears, even if `roomId` or other
// props change and trigger re-renders. The returned function tears down
// the connection when the component unmounts.
useEffectOnce(() => {
const conn = openConnection();
return () => conn.close();
});
return <div>Connected</div>;
}
// Across the component's life, the effect fires exactly one time:
// mount → effect runs (call count: 1)
// re-render → effect skipped (call count: 1)
// re-render → effect skipped (call count: 1)
// unmount → cleanup runs (cleanup count: 1)
effect returns a function, run it on unmount. If it returns nothing, unmount must not error.StrictMode, effects are not double-invoked, so the effect runs exactly once per mount.You'll wrap useEffect with an intentionally empty dependency array so the effect runs a single time after mount and its cleanup runs on unmount.
Some setup should happen exactly once when a component appears: open a socket, start a subscription, fire an analytics "screen viewed" event, kick off a one-time fetch. The catch is that components re-render constantly — a parent updates, a piece of state changes, a prop arrives — and you do not want that setup running again on every one of those renders. useEffectOnce gives you a single, named place to run that mount-time work once, and to tear it down when the component goes away.
A component's life is one mount, then any number of re-renders, then one unmount. You want your effect to attach to just the first of those events and your cleanup to attach to the last, ignoring everything in between. React already has a lever for this: an effect's dependency array tells React when to re-run the effect. List nothing, and there is nothing that can ever change, so React runs the effect on mount and the cleanup on unmount — and skips every render in between.
The obvious move is to just hand the effect straight to useEffect:
const { useEffect } = require('react');
function useEffectOnce(effect) {
useEffect(effect);
}
module.exports = { useEffectOnce };
This runs the effect after the first render — so far so good — but it also runs it after every render. With no dependency array, useEffect treats the effect as "depends on everything," so React re-runs it on each commit (and runs the cleanup before each re-run). A component that re-renders five times calls your "once" effect five times. The shape is right; the trigger is wrong.
const { useEffect } = require('react');
function useEffectOnce(effect) {
// The empty dependency array is the whole point: it tells React "this effect
// depends on nothing, so there is never a reason to re-run it." React runs it
// after the mount render and runs its cleanup (if any) on unmount — and skips
// every render in between.
useEffect(effect, []);
}
module.exports = { useEffectOnce };
The only change from the naive version is the second argument: []. That empty array is a promise to React that the effect reads nothing that can change between renders, so React has no reason to ever run it a second time. Whatever effect returns becomes the cleanup React stores and invokes on unmount; if it returns nothing, there is simply no cleanup to run.
Picture a Chat component whose effect opens a connection and returns () => conn.close():
Chat, commits it to the screen, then runs the effect once. The connection opens. React stores the returned () => conn.close() as this effect's cleanup.Chat re-renders. React checks the dependency array, sees [] — identical to last time, nothing changed — and skips the effect entirely. The connection stays open; no second connection is made.Chat is removed. React calls the stored cleanup once: conn.close(). The connection is torn down.Across the whole life of the component the effect ran once and the cleanup ran once, exactly as intended.
useEffect(effect) with no second argument runs on every render, not once. Fix: pass [] so the effect is tied to mount.if (!ran.current) { ran.current = true; effect(); } directly in the render body runs the effect during render rather than after commit, and never registers a cleanup for unmount. Fix: use useEffect(effect, []) so React handles both timing and teardown.effect returns nothing, there is no cleanup — unmount must not assume one exists. Passing the effect straight to useEffect handles this for free; React only calls a cleanup if the effect returned a function.useEffect with that value in its dependency array, not useEffectOnce.react-hooks/exhaustive-deps lint rule flags an empty array when the effect closes over props or state, because that effect will read stale values. useEffectOnce opts out of that check by design — document the intent so readers know the staleness is deliberate.useMount / useUnmount pair. Some libraries split this into two hooks — one for mount-only setup, one for unmount-only teardown — built on the same empty-array trick.StrictMode, React intentionally mounts, unmounts, and remounts components to surface missing cleanup, so the effect runs twice. That is a dev-only check; production and non-StrictMode trees run it once.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.