UI Patterns
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.

hooks/use-before-unload.ts
"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 beforeunload event 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. useBlocker in React Router).
  • event.preventDefault() is the standard. Calling event.preventDefault() is the spec-compliant way to trigger the prompt. Setting event.returnValue is kept for legacy browser support.
  • No effect during SSR. The effect only runs on the client because useEffect doesn'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 to sessionStorage and restore them when the page loads. This way, even if the user refreshes or navigates away, their input survives. The beforeunload prompt becomes a nice-to-have rather than the only safety net. Libraries like react-hook-form support this via custom storage adapters, or you can roll your own with a simple useEffect that debounces writes to sessionStorage.

Further reading

On this page