Skip to content

Prevent "back button" navigation #452

Description

@joecarl

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 beforeunload event. But it only works when the navigation was not made via pushState/setLocation.
Is it possible to achieve the behavior I need with wouter?

Activity

  1. molefrog commented on May 30, 2024

    @molefrog
    Owner

    You can try creating a custom hook that wraps default useBrowserLocation and checks for unsaved changes before actual navigate is 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

  2. joecarl commented on May 30, 2024

    @joecarl
    Author

    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.

  3. molefrog commented on May 30, 2024

    @molefrog
    Owner

    Hmm... I changed the sandbox visibility, could you try again?

  4. joecarl commented on May 30, 2024

    @joecarl
    Author

    Thankyou.

    Ok I tried the sandbox and it doesn't seem to address my issue. I followed these steps:

    1. Go to "About us".
    2. Dialog appears (nice), click confirm.
    3. Go to "Form" again.
    4. 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.
  5. ColonelThirtyTwo commented on Jul 27, 2024

    @ColonelThirtyTwo

    Back button won't go through the navigate function 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 every useLocation call adds another listener to the history popstate event.

  6. hajrudinco commented on Jul 18, 2025

    @hajrudinco

    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 useLockNavigation to 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();
        };
      }, []);
    };
  7. molefrog commented on Sep 5, 2026

    @molefrog
    Owner

    My earlier example doesn't intercept browser Back/Forward. #570 clarifies this in the docs; browser-history navigation blocking is still unresolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions