React cookbook · Intermediate

Warn users about unsaved changes

Prevent users from losing a half-filled form when navigating away or closing the tab.

Approach

Track dirtiness (e.g., React Hook Form's isDirty). Block in-app navigation with React Router's useBlocker and show a confirmation, and handle tab close/reload with the beforeunload event (browsers show their own generic message).

Solution

import { useEffect } from "react";
import { useBlocker } from "react-router";

export function useUnsavedChangesWarning(isDirty: boolean) {
  const blocker = useBlocker(({ currentLocation, nextLocation }) =>
    isDirty && currentLocation.pathname !== nextLocation.pathname,
  );

  useEffect(() => {
    if (!isDirty) return;
    const handler = (e: BeforeUnloadEvent) => { e.preventDefault(); };
    window.addEventListener("beforeunload", handler);
    return () => window.removeEventListener("beforeunload", handler);
  }, [isDirty]);

  return blocker;
}

export function EditProfile() {
  const { formState: { isDirty } } = useFormContext();
  const blocker = useUnsavedChangesWarning(isDirty);
  return (
    <>
      {/* form fields */}
      {blocker.state === "blocked" && (
        <div role="alertdialog" aria-label="Unsaved changes">
          <p>You have unsaved changes. Leave anyway?</p>
          <button onClick={() => blocker.reset()}>Stay</button>
          <button onClick={() => blocker.proceed()}>Leave</button>
        </div>
      )}
    </>
  );
}

Pitfalls

  • Always-on beforeunload handlers (hurt bfcache and annoy users).
  • Blocking navigation after a successful save (reset dirty state first).
  • Custom text in beforeunload (browsers ignore it).

Variations

  • Autosave drafts to localStorage or the server instead of blocking.

Further reading: React Router: useBlocker

Reference: https://reactrouter.com/api/hooks/useBlocker

More React recipes