useBlocker

A half-filled form and a misclicked link are a bad combination. useBlocker(shouldBlock) intercepts in-app navigations so you can ask first; it replaces React Router 5 139,386 's <Prompt>. The argument is a boolean or a function receiving { currentLocation, nextLocation, historyAction }; the hook returns a blocker whose state is "unblocked", "blocked" or "proceeding", with proceed() and reset() to resolve it.

Confirming before abandoning unsaved editsJavaScript
function DraftEditor() {
  const [dirty, setDirty] = useState(false);
  const blocker = useBlocker(({ currentLocation, nextLocation }) =>
    dirty && currentLocation.pathname !== nextLocation.pathname);
  return <div>
    <textarea onChange={(e) => setDirty(e.target.value.length > 0)} />
    {blocker.state === 'blocked' && <p role="alertdialog">You have unsaved changes.{' '}
      <button onClick={() => blocker.proceed()}>Leave anyway</button>{' '}
      <button onClick={() => blocker.reset()}>Keep editing</button></p>}</div>;
}

Comparing pathnames matters: without it, any query-string change useSearchParams writes counts as a navigation and pops the dialog. Call blocker.reset() when the blocking component unmounts or the form is saved, or a stale blocker can leave the router stuck in "blocked".

useBlocker only sees navigations the router controls. Closing the tab, reloading or typing a new address is beforeunload, which browsers restrict to a generic dialog and only after the user has interacted with the page, so a robust editor wires both. For a plain window.confirm() there is unstable_usePrompt({ when, message }), permanently prefixed because browsers disagree about the back button while a synchronous dialog is open.