Hooks
Before Unload
Prevent accidental page closes and navigations while a long-running process is in progress.
Examples
Dirty form guard
Warns the user before leaving the page when a form has unsaved changes.
No changes
While the badge says "Unsaved changes", try closing or refreshing this tab — the browser will ask for confirmation.
Long-running process
Warns the user while a simulated upload is in progress.
Idle
While the upload is in progress, try closing or refreshing — the browser will warn you.
"use client";
import * as React from "react";
/**
* Prevents the page from being closed or navigated away from while `active`
* is `true`. Uses the `beforeunload` event — the browser shows a
* confirmation dialog that cannot be customised.
*
* @param active — when `true`, the browser will prompt before unloading.
* @param message — ignored by modern browsers but set on the event for
* legacy compatibility.
*/
export function useBeforeUnload(active: boolean, message?: string) {
const messageRef = React.useRef(message);
messageRef.current = message;
React.useEffect(() => {
if (!active) return;
const handler = (event: BeforeUnloadEvent) => {
event.preventDefault();
// Legacy browsers require returnValue to be set.
// Modern browsers ignore the string but still show a prompt.
if (messageRef.current) {
event.returnValue = messageRef.current;
}
};
window.addEventListener("beforeunload", handler);
return () => window.removeEventListener("beforeunload", handler);
}, [active]);
}Usage:
// Guard a dirty form
const [dirty, setDirty] = useState(false);
useBeforeUnload(dirty);
// Guard a long-running process
const [uploading, setUploading] = useState(false);
useBeforeUnload(uploading);Notes:
- Browser-controlled dialog. Modern browsers ignore custom messages
passed to
beforeunload— the confirmation text is always the browser's default. This is a deliberate security decision to prevent sites from guilt-tripping users into staying. - Only fires on tab close / navigation. The
beforeunloadevent fires when the user closes the tab, closes the browser, refreshes, or navigates to a different URL. It does not fire for in-app SPA navigations (e.g. clicking a Next.js<Link>). For SPA route guards, use your router's built-in mechanism (e.g.useBlockerin React Router). event.preventDefault()is the standard. Callingevent.preventDefault()is the spec-compliant way to trigger the prompt. Settingevent.returnValueis kept for legacy browser support.- No effect during SSR. The effect only runs on the client because
useEffectdoesn't execute during server rendering. - Minimal API. The hook takes a single boolean — no need to manage listeners or cleanup manually. Toggle it on when the process starts, off when it finishes.
- Alternative: persist form state in
sessionStorage. Instead of (or in addition to) warning the user, you can periodically save form values tosessionStorageand restore them when the page loads. This way, even if the user refreshes or navigates away, their input survives. Thebeforeunloadprompt becomes a nice-to-have rather than the only safety net. Libraries likereact-hook-formsupport this via customstorageadapters, or you can roll your own with a simpleuseEffectthat debounces writes tosessionStorage.
Further reading
- MDN —
beforeunloadevent — the definitive reference, including browser differences. - Chrome for Developers —
beforeunloadchanges — why custom messages were removed from Chrome.