Skip to main content

useBlocker

Allow the application to block navigations within the SPA and present the user a confirmation dialog to confirm the navigation. Mostly used to avoid losing half-filled form data.
This hook only works in Data and Framework modes.
This does not handle hard-reloads or cross-origin navigations. Use the browser’s beforeunload event for those cases.

Signature

function useBlocker(
  shouldBlock: boolean | BlockerFunction
): Blocker

type BlockerFunction = (args: {
  currentLocation: Location;
  nextLocation: Location;
  historyAction: "PUSH" | "REPLACE" | "POP";
}) => boolean

interface Blocker {
  state: "unblocked" | "blocked" | "proceeding";
  location?: Location;
  proceed(): void;
  reset(): void;
}

Parameters

shouldBlock
boolean | BlockerFunction
required
Either a boolean or a function that returns a boolean indicating whether the navigation should be blocked.When using the function format, it receives:
  • currentLocation - The current location
  • nextLocation - The location being navigated to
  • historyAction - The type of navigation (PUSH, REPLACE, or POP)

Returns

blocker
Blocker
An object with the following properties:

Usage

Block with boolean

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

function Form() {
  const [value, setValue] = useState("");
  const blocker = useBlocker(value !== "");
  
  return (
    <>
      <form>
        <input
          value={value}
          onChange={(e) => setValue(e.target.value)}
        />
      </form>
      
      {blocker.state === "blocked" && (
        <div className="modal">
          <p>You have unsaved changes. Are you sure you want to leave?</p>
          <button onClick={blocker.proceed}>Leave</button>
          <button onClick={blocker.reset}>Stay</button>
        </div>
      )}
    </>
  );
}

Block with function

import { useBlocker } from "react-router";
import { useState, useCallback } from "react";

function Form() {
  const [isDirty, setIsDirty] = useState(false);
  
  const shouldBlock = useCallback(
    ({ currentLocation, nextLocation }) => {
      // Only block if form is dirty and navigating away
      return (
        isDirty &&
        currentLocation.pathname !== nextLocation.pathname
      );
    },
    [isDirty]
  );
  
  const blocker = useBlocker(shouldBlock);
  
  return (
    <form
      onChange={() => setIsDirty(true)}
      onSubmit={() => setIsDirty(false)}
    >
      {/* form fields */}
      
      {blocker.state === "blocked" && (
        <ConfirmDialog blocker={blocker} />
      )}
    </form>
  );
}

Proceed on form submit

function ImportantForm() {
  const [value, setValue] = useState("");
  const blocker = useBlocker(value !== "");
  
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        setValue("");
        
        // If blocked, proceed after saving
        if (blocker.state === "blocked") {
          blocker.proceed();
        }
      }}
    >
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <button type="submit">Save</button>
      
      {blocker.state === "blocked" && (
        <div className="dialog">
          <p>Blocked navigation to {blocker.location?.pathname}</p>
          <button onClick={blocker.proceed}>Leave without saving</button>
          <button onClick={blocker.reset}>Keep editing</button>
        </div>
      )}
    </form>
  );
}

Custom confirmation dialog

function ConfirmDialog({ blocker }) {
  if (blocker.state !== "blocked") return null;
  
  return (
    <div className="modal-overlay">
      <div className="modal">
        <h2>Unsaved Changes</h2>
        <p>
          You have unsaved changes. Do you want to leave without saving?
        </p>
        
        <div className="actions">
          <button
            onClick={blocker.reset}
            className="primary"
          >
            Stay and Save
          </button>
          <button
            onClick={blocker.proceed}
            className="danger"
          >
            Leave without Saving
          </button>
        </div>
      </div>
    </div>
  );
}

function Form() {
  const [isDirty, setIsDirty] = useState(false);
  const blocker = useBlocker(isDirty);
  
  return (
    <>
      <form onChange={() => setIsDirty(true)}>
        {/* form fields */}
      </form>
      
      <ConfirmDialog blocker={blocker} />
    </>
  );
}

Common Patterns

Block only external navigation

function Form() {
  const [isDirty, setIsDirty] = useState(false);
  const location = useLocation();
  
  const shouldBlock = useCallback(
    ({ nextLocation }) => {
      // Only block if leaving the form section
      return (
        isDirty &&
        !nextLocation.pathname.startsWith("/forms")
      );
    },
    [isDirty]
  );
  
  const blocker = useBlocker(shouldBlock);
  
  return <form onChange={() => setIsDirty(true)}>...</form>;
}

Show destination in dialog

function Form() {
  const [value, setValue] = useState("");
  const blocker = useBlocker(value !== "");
  
  return (
    <>
      <form>
        <input
          value={value}
          onChange={(e) => setValue(e.target.value)}
        />
      </form>
      
      {blocker.state === "blocked" && (
        <div className="modal">
          <p>
            You're trying to navigate to{" "}
            <strong>{blocker.location?.pathname}</strong>
          </p>
          <p>Unsaved changes will be lost.</p>
          <button onClick={blocker.proceed}>Continue</button>
          <button onClick={blocker.reset}>Cancel</button>
        </div>
      )}
    </>
  );
}

Auto-save before proceeding

function AutoSaveForm() {
  const [value, setValue] = useState("");
  const [isSaving, setIsSaving] = useState(false);
  const blocker = useBlocker(value !== "");
  
  const saveAndProceed = async () => {
    setIsSaving(true);
    await saveData(value);
    setIsSaving(false);
    setValue("");
    blocker.proceed();
  };
  
  return (
    <>
      <form>
        <input
          value={value}
          onChange={(e) => setValue(e.target.value)}
        />
      </form>
      
      {blocker.state === "blocked" && (
        <div className="modal">
          <p>Save your changes before leaving?</p>
          <button onClick={saveAndProceed} disabled={isSaving}>
            {isSaving ? "Saving..." : "Save and Leave"}
          </button>
          <button onClick={blocker.proceed}>Leave without Saving</button>
          <button onClick={blocker.reset}>Stay</button>
        </div>
      )}
    </>
  );
}

Multi-step wizard

function Wizard() {
  const [step, setStep] = useState(1);
  const [data, setData] = useState({});
  const isComplete = step === 3;
  
  const blocker = useBlocker(
    useCallback(
      ({ nextLocation }) => {
        // Block if wizard incomplete and leaving wizard
        return !isComplete && !nextLocation.pathname.startsWith("/wizard");
      },
      [isComplete]
    )
  );
  
  return (
    <div>
      <h1>Step {step} of 3</h1>
      {/* Step content */}
      
      <button onClick={() => setStep(step + 1)}>Next</button>
      
      {blocker.state === "blocked" && (
        <div className="modal">
          <p>You haven't completed the wizard. Progress will be lost.</p>
          <button onClick={blocker.proceed}>Leave</button>
          <button onClick={blocker.reset}>Continue Wizard</button>
        </div>
      )}
    </div>
  );
}

Block during async operations

function Component() {
  const [isUploading, setIsUploading] = useState(false);
  const blocker = useBlocker(isUploading);
  
  const handleUpload = async (file: File) => {
    setIsUploading(true);
    await uploadFile(file);
    setIsUploading(false);
  };
  
  return (
    <>
      <input
        type="file"
        onChange={(e) => handleUpload(e.target.files[0])}
      />
      
      {blocker.state === "blocked" && (
        <div className="modal">
          <p>Upload in progress. Are you sure you want to cancel?</p>
          <button onClick={blocker.reset}>Wait for Upload</button>
          <button onClick={blocker.proceed}>Cancel Upload</button>
        </div>
      )}
    </>
  );
}

Important Notes

Browser navigation

useBlocker only blocks in-app navigation (using <Link>, navigate(), etc.). It does NOT block:
  • Browser back/forward buttons (use beforeunload event)
  • Page refreshes (use beforeunload event)
  • Closing the tab/window (use beforeunload event)
  • External links
For those cases, use the browser’s beforeunload event:
useEffect(() => {
  const handleBeforeUnload = (e: BeforeUnloadEvent) => {
    if (isDirty) {
      e.preventDefault();
      e.returnValue = "";
    }
  };
  
  window.addEventListener("beforeunload", handleBeforeUnload);
  return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [isDirty]);

Stable function reference

When using a function for shouldBlock, use useCallback to ensure a stable reference:
const shouldBlock = useCallback(
  ({ currentLocation, nextLocation }) => {
    return isDirty && currentLocation.pathname !== nextLocation.pathname;
  },
  [isDirty]
);

const blocker = useBlocker(shouldBlock);

State transitions

unblocked --[navigation attempted while shouldBlock=true]--> blocked
blocked --[proceed()]--> proceeding --> unblocked
blocked --[reset()]--> unblocked

Build docs developers (and LLMs) love