DEV Community

ReactUse
ReactUse

Posted on • Originally published at reactuse.com

React useMount Hook: Run an Effect Once on Mount (2026)

"Run this once, when the component appears." It's the single most common effect in React — focus an input, fire an analytics event, open a connection, read from a browser API. The idiom everyone reaches for is useEffect with an empty dependency array:

useEffect(() => {
  trackPageView("/checkout");
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Enter fullscreen mode Exit fullscreen mode

It works, but look at what it costs: an empty array you must remember (forget it and the effect runs on every render), a lint suppression comment whenever the effect touches anything, and — worst of all — zero stated intent. useEffect(fn, []) says how; it never says why. Six months later, a teammate adds a dependency to that array "to fix the lint warning" and your run-once effect quietly becomes a run-on-change effect.

useMount from @reactuses/core is the named version of this idiom: useMount(fn) runs fn exactly once per mount, and the name is the documentation. This post covers what it actually compiles down to, the React 18+ StrictMode double-run that surprises everyone the first time, the subtle stale-closure bug in hand-rolled unmount cleanups (and how useUnmount dodges it), async work on mount, and — just as important — the cases where you should not use it.

Quick Start

npm install @reactuses/core
Enter fullscreen mode Exit fullscreen mode
import { useMount, useUnmount } from "@reactuses/core";
import { useRef } from "react";

function SearchBox() {
  const inputRef = useRef<HTMLInputElement>(null);

  useMount(() => {
    inputRef.current?.focus();
  });

  useUnmount(() => {
    console.log("search box removed");
  });

  return <input ref={inputRef} placeholder="Search…" />;
}
Enter fullscreen mode Exit fullscreen mode

No dependency array, no lint comment, and the reader knows the intent before reading the body: this runs on mount, full stop.

What useMount Actually Is

No magic — here is the entire implementation, minus a dev-only type check:

export const useMount = (fn: () => void) => {
  useEffect(() => {
    fn?.();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
};
Enter fullscreen mode Exit fullscreen mode

That's it: useEffect with an empty array, wrapped once so you never write the array or the suppression comment again. Three things fall out of this five-line definition:

  1. Timing is useEffect timing. The callback fires after the component is committed to the DOM — after first paint, browser APIs available. It is not useLayoutEffect; if you need to measure and mutate before paint, reach for a layout effect instead.
  2. It is SSR-safe by construction. Effects simply never run on the server, so useMount is the natural home for window/document access in SSR apps — the same guarantee the manual useEffect(fn, []) gives you, with the intent spelled out.
  3. The return value is ignored. useMount calls fn?.() and discards the result — it does not forward a returned function to React as cleanup. Cleanup belongs to useUnmount (below). A side effect of this design: passing an async function is safe, which is more than you can say for raw useEffect (more on that in a minute).

One consequence to internalize: because the dependency array is empty, the callback closes over first-render values. Props and state read inside useMount are frozen at their initial values. For a mount effect that's almost always what you want — but if you find yourself wanting fresh values inside, that's the signal you actually want useEffect with dependencies, or a useLatest ref.

The StrictMode Gotcha: "Why Does My Mount Effect Run Twice?"

Search "useEffect runs twice" and you'll find a decade of confusion. Here's the short version: since React 18, <StrictMode> in development deliberately mounts every component, unmounts it, and mounts it again. Any mount effect — useEffect(fn, []), useMount, doesn't matter — runs twice in dev. In production it runs once.

React does this on purpose, to surface effects that don't clean up after themselves. The official guidance is: don't fight the double-run, make the effect idempotent — running it twice should be harmless because the cleanup undoes the first run:

useMount(() => {
  const controller = new AbortController();
  fetch("/api/config", { signal: controller.signal }).then(applyConfig);
  // pair with useUnmount(() => controller.abort())
});
Enter fullscreen mode Exit fullscreen mode

But some effects are genuinely once-only, and running them twice is a real bug, not a hygiene warning: an analytics beacon fires twice, a welcome toast pops twice, a payment-intent gets created twice in dev and QA files a ticket. For those, @reactuses/core ships useOnceEffect:

import { useOnceEffect } from "@reactuses/core";

useOnceEffect(() => {
  trackPageView("/checkout"); // fires once, even under StrictMode
});
Enter fullscreen mode Exit fullscreen mode

The trick inside is elegant: useOnceEffect records each effect function in a WeakSet before running it. StrictMode's remount re-invokes the same effect function instance from the same render, so the second invocation finds it already recorded and bails. Genuine remounts (the component actually left and came back) create a fresh function and run again — exactly the semantics "once per mount, ignoring StrictMode's rehearsal" implies.

Rule of thumb: useMount + idempotent by default; useOnceEffect when a double-fire is observable to a user or a backend.

useUnmount — and the Stale-Closure Trap It Avoids

The obvious hand-rolled unmount cleanup has a bug most people ship without noticing:

// ⚠️ hand-rolled version
useEffect(() => {
  return () => {
    saveDraft(draft); // draft from the FIRST render — always empty!
  };
}, []); // empty deps ⇒ the cleanup closure was created on render #1
Enter fullscreen mode Exit fullscreen mode

The cleanup function was created on the first render, so it captured the first render's draft. When the component unmounts three minutes and forty keystrokes later, it saves an empty string. The "fix" of adding draft to the deps is worse — now the cleanup runs on every keystroke, not on unmount.

useUnmount solves this properly. Internally it stores your callback in a useLatest ref that's updated every render, and the unmount cleanup calls through the ref:

import { useUnmount } from "@reactuses/core";

function Composer() {
  const [draft, setDraft] = useState("");

  useUnmount(() => {
    saveDraft(draft); // ✅ the draft as of the LAST render
  });

  return <textarea value={draft} onChange={e => setDraft(e.target.value)} />;
}
Enter fullscreen mode Exit fullscreen mode

Your callback runs exactly once, at unmount, and sees the latest state. This is the concrete reason to prefer useUnmount over the return () => {} idiom whenever the cleanup reads state or props — it's not sugar, it's a bug fix.

Async Work on Mount

Raw useEffect famously rejects async functions — useEffect(async () => {...}, []) hands React a Promise where it expects a cleanup function, and you get a warning plus a skipped cleanup. Because useMount discards the callback's return value, an async callback is perfectly fine:

useMount(async () => {
  const user = await fetchCurrentUser();
  setUser(user);
});
Enter fullscreen mode Exit fullscreen mode

One thing this doesn't give you is protection against the component unmounting mid-await — calling setUser after unmount is harmless in React 18+ but often still not what you want (you may be writing to state that a remounted instance will clobber). Two library answers:

  • useMountedState returns an isMounted() function backed by a ref — check it after each await:
  const isMounted = useMountedState();

  useMount(async () => {
    const user = await fetchCurrentUser();
    if (isMounted()) setUser(user);
  });
Enter fullscreen mode Exit fullscreen mode
  • useAsyncEffect generalizes the pattern for effects with dependencies, handing your async body a liveness check and supporting cleanup.

For real data fetching with caching and retries you'll outgrow both — that's React Query / SWR territory, or your framework's loaders. useMount is for the one-shot imperative stuff around the edges.

The Mirror Image: Skipping the Mount

Sometimes you want the opposite — react to changes but not to the initial mount. Sync a filter to the URL, but don't rewrite the URL on first load; show "settings saved" on change, but not on arrival. That's useUpdateEffect, and its primitive sibling useFirstMountState which simply tells you whether this is the first render:

import { useUpdateEffect } from "@reactuses/core";

useUpdateEffect(() => {
  syncFilterToUrl(filter); // runs on filter changes, skips the mount
}, [filter]);
Enter fullscreen mode Exit fullscreen mode

Together the four hooks cover the whole lifecycle vocabulary that classes used to spell componentDidMount / componentDidUpdate / componentWillUnmount:

You want to run code… Reach for
once, after the component appears useMount
once, even under StrictMode's dev double-run useOnceEffect
when the component is removed, seeing latest state useUnmount
on updates only, skipping the first render useUpdateEffect
conditionally, based on "is this the first render?" useFirstMountState

When Not to Use useMount

Honesty section. useMount is intent-naming sugar over a real React primitive, and the primitive is sometimes the right call:

  • The effect reads a prop or state that can change. If roomId changes and you need to reconnect, that is useEffect(connect, [roomId]) — a mount hook here is a synchronization bug wearing a convenience API. The empty array isn't ceremony in that case; it's wrong.
  • You're fetching server data for rendering. Framework loaders, React Query, SWR — anything with caching, deduplication, and revalidation beats a fetch in a mount effect. React's own docs have walked away from "fetch in useEffect" as a primary pattern.
  • You need pre-paint measurement. useMount is post-paint. Measure-then-mutate work belongs in a layout effect.
  • The "mount event" is actually a user event. If code can run in the click handler that caused the component to appear, run it there — effects are for synchronizing with external systems, not a junk drawer.

The test is one question: would this code ever need to re-run while the component is alive? If the answer is any form of "yes, when X changes," you want useEffect and a dependency array. If it's a clean "no," useMount says so in a way [] never will.

Takeaways

  • useMount(fn) is useEffect(fn, []) with the intent in the name — no array to forget, no lint suppression, first-render closure semantics you should embrace, not fight.
  • In React 18+ dev StrictMode every mount effect runs twice. Make effects idempotent by default; use useOnceEffect when a double-fire is user- or backend-visible.
  • Hand-rolled return () => {} cleanups with empty deps capture first-render state — a real, shipping bug. useUnmount reads through a latest-ref and sees final state.
  • useMount accepts async callbacks (the return value is discarded); guard post-await state writes with useMountedState or use useAsyncEffect.
  • For change-only effects, useUpdateEffect is the mirror image.

useMount, useUnmount, useOnceEffect, and 110+ other SSR-safe, TypeScript-first hooks live in @reactuses/core — one install, tree-shakeable, no dependencies to babysit.

npm install @reactuses/core
Enter fullscreen mode Exit fullscreen mode

Top comments (0)