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
An object with the following properties: state
'unblocked' | 'blocked' | 'proceeding'
The current blocker state:
"unblocked" - The blocker is idle and has not prevented any navigation
"blocked" - The blocker has prevented a navigation
"proceeding" - The blocker is proceeding through from a blocked navigation
When in a "blocked" state, this represents the location to which navigation was blocked. When in a "proceeding" state, this is the location being navigated to after a proceed() call.
When in a "blocked" state, call this function to proceed to the blocked location.
When in a "blocked" state, call this function to return the blocker to an "unblocked" state and leave the user at the current location.
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 >
);
}
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