Repository navigation
Prevent "back button" navigation #452
Description
Activity
You can try creating a custom hook that wraps default
useBrowserLocationand checks for unsaved changes before actualnavigateis called. Here, I found the demo that was initially written for V2 but I think it still works:https://codesandbox.io/s/wouter-demo-interruptible-transition-v3-phrcz4?file=/src/index.js
Thanks for your reply, but the provided link is not loading :(
Anyway, are you sure that navigation caused by "browser's back button" can be prevented?
I tried to create the custom hook as you suggested but I'm definetely missing some info.Hmm... I changed the sandbox visibility, could you try again?
Thankyou.
Ok I tried the sandbox and it doesn't seem to address my issue. I followed these steps:
- Go to "About us".
- Dialog appears (nice), click confirm.
- Go to "Form" again.
- Then hit back button. At this point I would expect a confirmation dialog, but navigation occurs without confirmation. This is the issue I'm facing.
Reacted by Santiago Sánchez Sobrino, Felix Spöttel, Valdrin Trena, Kusou1, Petter, Corey M. and Lucas TeixeiraBack button won't go through the
navigatefunction that's overwritten in the snippet above.react-router seems to handle back/forward by immediately undoing the navigation via
history.go(delta * -1), but that approach is hard to do with wouter's design where everyuseLocationcall adds another listener to the history popstate event.By customizing location and search hook you can achieve what you want without overriding or patching history.
For example, your custom hooks can keep actual application location and search value internally, and let browser do what it does. After detecting that browser changed the location, you can ask the user for confirmation in case the lock was on.
If user confirms, you update internal state and application will render the new route. If user cancels, you can return (rollback) browser location to the current location that your application is keeping internally. This way there is no need to patch browser behavior or even listen to the popstate.
Browser can do anything and your application will either accept or decline location change done by the browser.
You can see the example here: https://codesandbox.io/p/devbox/wouter-demo-interruptible-transition-v3-forked-h4cdq6
Make sure to open and test the preview in a separate tab: https://h4cdq6.csb.app/ , as manipulating navigation/history through iframe directly in sandbox is unpredictable.
You need to wrap your application with the
AppNavigatorRouter:<AppNavigatorRouter> <MyApp /> </AppNavigatorRouter>
and then in your component, use
useLockNavigationto set the flag:useLockNavigation({ lock: true, });
This is how these were constructed:
import { useEffect, useState, FC, PropsWithChildren } from "react"; import { BaseLocationHook, BaseSearchHook, Router, RouterProps } from "wouter"; import { useBrowserLocation, useSearch as useBrowserSearch, } from "wouter/use-browser-location"; type AppLocationSubscriber = (location: string, search: string) => void; type AppLock = { lock: boolean; preventPageLeave?: boolean; message?: string; }; const DEFAULT_MESSAGE = "You have unsaved changes. Are you sure you want to leave?"; class AppNavigator { private appLocation: string; private appSearch: string = ""; private subscribers = new Set<AppLocationSubscriber>(); private lock = false; private preventPageLeave = false; private message = ""; constructor() { this.appLocation = window.location.pathname; this.appSearch = window.location.search || ""; this.watchWindowUnloadEvent(); } private getMessage() { return this.message || DEFAULT_MESSAGE; } private watchWindowUnloadEvent() { window.addEventListener("beforeunload", (event) => { if (!this.preventPageLeave) { return; } event.preventDefault(); event.returnValue = this.getMessage(); // For legacy browsers return this.getMessage(); // For modern browsers }); } public setLock({ lock, preventPageLeave, message }: AppLock) { this.lock = lock; this.preventPageLeave = typeof preventPageLeave === "boolean" ? preventPageLeave : lock; this.message = message || ""; } public unlock() { this.lock = false; this.preventPageLeave = false; this.message = ""; } public getLocation() { return this.appLocation; } public getSearch() { return this.appSearch; } public confirmTransition(): boolean { if (!this.lock) { return true; } return confirm(this.getMessage()); } public updateAppLocation(nextLocation: string, nextSearch: string) { this.appLocation = nextLocation; this.appSearch = nextSearch || ""; this.subscribers.forEach((fn) => fn(this.appLocation, this.appSearch)); } public subscribe(fn: AppLocationSubscriber) { this.subscribers.add(fn); return () => this.subscribers.delete(fn); } } const appNavigator = new AppNavigator(); const useAppLocation: BaseLocationHook = () => { const [, setBrowserLocation] = useBrowserLocation(); const [appLocation, setAppLocation] = useState(appNavigator.getLocation()); useEffect(() => { const unsubscribe = appNavigator.subscribe((location) => { setAppLocation(location); }); return () => { unsubscribe(); }; }, []); return [appLocation, setBrowserLocation]; }; const useAppSearch: BaseSearchHook = () => { const [appSearch, setAppSearch] = useState(appNavigator.getSearch()); useEffect(() => { const unsubscribe = appNavigator.subscribe((_, search) => setAppSearch(search) ); return () => { unsubscribe(); }; }, []); return appSearch; }; export const useAppNavigator = () => { const [browserLocation, setBrowserLocation] = useBrowserLocation(); const browserSearch = useBrowserSearch(); useEffect(() => { if ( browserLocation + browserSearch === appNavigator.getLocation() + appNavigator.getSearch() ) { return; } if (appNavigator.confirmTransition()) { // Accept the transition and update app state appNavigator.updateAppLocation(browserLocation, browserSearch); } else { // Revert the browser's location if user cancels setBrowserLocation(appNavigator.getLocation() + appNavigator.getSearch()); } }, [browserLocation, browserSearch, setBrowserLocation]); }; export const AppNavigatorRouter: FC<PropsWithChildren<RouterProps>> = ({ children, ...routerProps }) => { useAppNavigator(); return ( <Router hook={useAppLocation} searchHook={useAppSearch} {...routerProps}> {children} </Router> ); }; export const useLockNavigation = ({ lock, preventPageLeave, message, }: AppLock) => { useEffect(() => { appNavigator.setLock({ lock, preventPageLeave, message, }); }, [lock, preventPageLeave, message]); useEffect(() => { return () => { appNavigator.unlock(); }; }, []); };
Reacted by Samuel Karani- added a commit that references this issue
on Jun 30, 2026 - added a commit that references this issue
on Sep 5, 2026 My earlier example doesn't intercept browser Back/Forward. #570 clarifies this in the docs; browser-history navigation blocking is still unresolved.
I'm developing an app where I need to prevent "back button" in some cases, (e.g.: when I'm editing a form and I click back button I should receive a confirmation dialog before the navigation occurs).
I tried adding the well known
beforeunloadevent. But it only works when the navigation was not made viapushState/setLocation.Is it possible to achieve the behavior I need with wouter?