Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Browser Back Button does not trigger window.onbeforeunload #2694

Closed
1 task done
zakbutcher opened this issue Aug 1, 2017 · 33 comments
Closed
1 task done

Browser Back Button does not trigger window.onbeforeunload #2694

zakbutcher opened this issue Aug 1, 2017 · 33 comments

Comments

@zakbutcher
Copy link

zakbutcher commented Aug 1, 2017

In our application we have forms and want to prompt the user when they click the browser's Back or Forward buttons to notify them they will lose their data if they continue.

  • I have searched the issues of this repository and believe that this is not a duplicate.

Although it is similar in nature to this issue that has been marked as Exploration

Expected Behavior

When user clicks back button, window.onbeforeunload function that is defined fires

Current Behavior

Browser redirects user to previous page without triggering window.onbeforeunload action

Special Notes

If I navigate directly to the page with the form by typing in the url, clicking the Back button correctly triggers window.onbeforeunload.

If I navigate away from the page with the form by typing in the url for another page, the window.onbeforeunload event fires correctly.

If I navigate to the page using links within the application, clicking the Back button does not trigger window.onbeforeunload.

I think this has something to do with they way the browser is treating our application when the user navigates within the application using Next Links. Instead of seeing each view as its own page and loading/unloading the page resources as the user navigates, it sees the entire application as just one page, thereby never loading/unloading resources and thus not triggering window.onbeforeunload.

@timneutkens timneutkens added Type: Bug good first issue Easy to fix issues, good for newcomers and removed Type: Bug labels Nov 12, 2017
@liweinan0423
Copy link
Contributor

liweinan0423 commented Nov 15, 2017

The beforeunload event gets triggered when the window is about to unload its resources, it happens when user is navigating to another page via a Full Page Reload

One of the most important difference between SPA and multip-page websites is that the window will never unload its resources when user is navigating between different "views" in a SPA. The browser never does full-page reload unless user is hitting the "refresh" button.

next.js provide some callback such as Router.onRouteChangeStart but it doesn't support stopping the transition if the callback function returns false, I think we should support this

See #2236

@ppatel221
Copy link

I'd be interested in fixing this bug. Can I take over?

@arunoda
Copy link
Contributor

arunoda commented Dec 8, 2017

@ppatel221 yes. You can take this.
I think we should not call or trigger window.onbeforeunload, but a some function of Router

@jameskennethrobinson
Copy link

Very interested in this feature. Would greatly improve experience on forms in my side project.

@tamer-mohamed
Copy link

any updates on this?

@RIP21
Copy link

RIP21 commented Jun 25, 2018

Up. Hopefully, somebody will fix that.

@timneutkens
Copy link
Member

Note that there is a router event for going back now: https://github.com/zeit/next.js#intercepting-popstate

@rameshanandakrishnan
Copy link

rameshanandakrishnan commented Jun 28, 2018

@timneutkens Intercepting popstate will work when the user clicks the back button or forward button but will not work if a user clicks on a link to navigate to another page. Is it possible to have beforeChange function which accepts a callback which contains a window.confirm() and depending on the response we can decide to block the change and emit routerChangeBlocked?

@DonovanCharpin
Copy link

Agree with @rameshanandakrishnan, the popstate handler is great when going back but not for click handling. The beforeRouteChangeStart which is proposed in the PR would be exactly what I need to be able to intercept any route change and cancel it if needed. I think it's kind of mandatory for big forms to avoid loosing it.

@timneutkens
Copy link
Member

As per #5377 (comment)

@timneutkens timneutkens removed the good first issue Easy to fix issues, good for newcomers label Nov 2, 2018
@ajhool

This comment has been minimized.

@ahtashamabbasse
Copy link

Following code work for me. 🚀

const handleWindowClose = (e) => {
    if (isDirty) {
      e.preventDefault();
      return e.returnValue = 'You have unsaved changes - are you sure you wish to close?';
    }

  };

  React.useEffect(() => {
    window.addEventListener('beforeunload', handleWindowClose);

    return () => {
      window.removeEventListener('beforeunload', handleWindowClose);
    };

  });

@AvinashSoorineedi
Copy link

In our application we have forms and want to prompt the user when they click the browser's Back or Forward buttons to notify them they will lose their data if they continue.

  • I have searched the issues of this repository and believe that this is not a duplicate.

Although it is similar in nature to this issue that has been marked as Exploration

Expected Behavior

When user clicks back button, window.onbeforeunload function that is defined fires

Current Behavior

Browser redirects user to previous page without triggering window.onbeforeunload action

Special Notes

If I navigate directly to the page with the form by typing in the url, clicking the Back button correctly triggers window.onbeforeunload.

If I navigate away from the page with the form by typing in the url for another page, the window.onbeforeunload event fires correctly.

If I navigate to the page using links within the application, clicking the Back button does not trigger window.onbeforeunload.

I think this has something to do with they way the browser is treating our application when the user navigates within the application using Next Links. Instead of seeing each view as its own page and loading/unloading the page resources as the user navigates, it sees the entire application as just one page, thereby never loading/unloading resources and thus not triggering window.onbeforeunload.

@zakbutcher how did you solve this issue finally?

@Nitaaq
Copy link

Nitaaq commented Sep 27, 2020

facing this problem too, anybody came up with a fix ? its been 3 years now :/

@ahtashamabbasse
Copy link

Following code work for me. 🚀

const handleWindowClose = (e) => {
    if (isDirty) {
      e.preventDefault();
      return e.returnValue = 'You have unsaved changes - are you sure you wish to close?';
    }

  };

  React.useEffect(() => {
    window.addEventListener('beforeunload', handleWindowClose);

    return () => {
      window.removeEventListener('beforeunload', handleWindowClose);
    };

  });

@Nitaaq Try this snippet that will work.

@Nitaaq
Copy link

Nitaaq commented Sep 30, 2020

@ahtashamabbasse it will work, but instead of a native alert i'd like to show a custom html dialog, i was able to do that too but whenever the user presses the back button that is in the browser, the link in the address bar changes and the custom dialog appears which is not the intended behavior. the thing i want to achieve is the same as react-router s Prompt

@Fensterbank
Copy link

Fensterbank commented Nov 17, 2020

@ahtashamabbasse Your snippet does not address this issue at all.
It's just the standard usage of the beforeunload event, but this issue exists because that window event never gets called on a navigation inside of the Next app.
So while your snippet works for refreshing the page or closing the tab, it's unfortunately only half the solution.

There is a good discussion on this issue, where the example of emiting routeChangeError and throwing an error is written.
Unfortunately it doesn't look like there will be an official support for this, which is kind of sad since preventing lost of unsaved change is a basic feature in the web since forever.

This is the workaround I used and it seems to work so far:

  // Use beforeunload to prevent closing the tab, refreshing the page or moving outside the Next app
  useEffect(() => {
    window.addEventListener('beforeunload', handleWindowClose);

    return () => {
      window.removeEventListener('beforeunload', handleWindowClose);
    };
  });

  // Use routeChangeStart to prevent navigation inside of the Next app
  // Uses a module variable to bypass the confirm, otherwise we would be in a loop
  router.events.on('routeChangeStart', () => {
    if (leaveConfirmed) return;

    if (window.confirm('Are you sure you want to leave this page?'))
      leaveConfirmed = true;
    else {
      router.events.emit('routeChangeError');
      throw 'routeChange aborted.';
    }
  });

  // Set the module variable to false on component mount
  useEffect(() => {
    leaveConfirmed = false;
  }, []);

@roderickhodgson
Copy link

roderickhodgson commented Nov 24, 2020

Thanks for that snippet @Fensterbank. Super useful.

I did notice one possible issue with it. Which I think is what prompted you to use the leaveConfirmed variable.

The function gets added as a listener to the event each time the component is rendered. So the page may end up creating hundreds of copies of that function, and calling each one of those copies when a user browses internally. And these never get cleared. Of course that might not be an issue in many use cases. But it was a problem with my particular scenario. Either way, I might suggest adding the router event listener in an event hook, just like the window beforeunload listener.

Here is a version with this approach, for anyone who gets to this page looking for another solution. Note, I have adapted it a bit further for my requirements.

  // prompt the user if they try and leave with unsaved changes
  useEffect(() => {
    const warningText =
      'You have unsaved changes - are you sure you wish to leave this page?';
    const handleWindowClose = (e: BeforeUnloadEvent) => {
      if (!unsavedChanges) return;
      e.preventDefault();
      return (e.returnValue = warningText);
    };
    const handleBrowseAway = () => {
      if (!unsavedChanges) return;
      if (window.confirm(warningText)) return;
      router.events.emit('routeChangeError');
      throw 'routeChange aborted.';
    };
    window.addEventListener('beforeunload', handleWindowClose);
    router.events.on('routeChangeStart', handleBrowseAway);
    return () => {
      window.removeEventListener('beforeunload', handleWindowClose);
      router.events.off('routeChangeStart', handleBrowseAway);
    };
  }, [unsavedChanges]);

So far, it seems to work pretty reliably.

@Fensterbank
Copy link

@roderickhodgson Thanks for your reply. Good catch, after reading this I'm pretty sure exactly this was the case why I just dirty bypassed this with the module variable. 🙈

I will switch to your approach.

@lcanavesio
Copy link

HI @roderickhodgson ! Thanks for sharing the code works correctly for me. But how do I detect a type = submit? In that case I shouldn't be blocked.

@kashkashio
Copy link

Thanks for that snippet @Fensterbank. Super useful.

I did notice one possible issue with it. Which I think is what prompted you to use the leaveConfirmed variable.

The function gets added as a listener to the event each time the component is rendered. So the page may end up creating hundreds of copies of that function, and calling each one of those copies when a user browses internally. And these never get cleared. Of course that might not be an issue in many use cases. But it was a problem with my particular scenario. Either way, I might suggest adding the router event listener in an event hook, just like the window beforeunload listener.

Here is a version with this approach, for anyone who gets to this page looking for another solution. Note, I have adapted it a bit further for my requirements.

  // prompt the user if they try and leave with unsaved changes
  useEffect(() => {
    const warningText =
      'You have unsaved changes - are you sure you wish to leave this page?';
    const handleWindowClose = (e: BeforeUnloadEvent) => {
      if (!unsavedChanges) return;
      e.preventDefault();
      return (e.returnValue = warningText);
    };
    const handleBrowseAway = () => {
      if (!unsavedChanges) return;
      if (window.confirm(warningText)) return;
      router.events.emit('routeChangeError');
      throw 'routeChange aborted.';
    };
    window.addEventListener('beforeunload', handleWindowClose);
    router.events.on('routeChangeStart', handleBrowseAway);
    return () => {
      window.removeEventListener('beforeunload', handleWindowClose);
      router.events.off('routeChangeStart', handleBrowseAway);
    };
  }, [unsavedChanges]);

So far, it seems to work pretty reliably.

This is gold.
Life save thank you !

@Tymek
Copy link

Tymek commented Apr 14, 2021

Great contribution. Thx @roderickhodgson 🙌🏻
Reusable hook version of this answer: https://gist.github.com/Tymek/df2021b77fcea20cabaef46bbee8b001

@anetecima
Copy link

The solution provided by @roderickhodgson Isn't really working out for me.

it does block navigation really well in all the mentioned cases, but the issue I have is with window.history and pathname.

Is there a solution for handling the page state after throwing an error in the "routeChangeStart" when triggered by navigating back via browser the same as in "beforeunload" when page state is returned if the user chooses to stay on the page.

@eric-burel
Copy link
Contributor

eric-burel commented May 21, 2021

I see a lot of solutions that depends on the router. But this is just trading a dependency from React Router to Next Router, we will have the same issue in 10 year when there is a new version of the Router and we must change everything.

I don't really see how it could be better decoupled, maybe using events?

  • Next router triggers an event on route change (it seems to be the case), we can listen to it
  • Next should listen for a potential blocking event, that any piece of code could trigger
    Using events you can decouple things, you don't need to know the underlying router implementation to trigger the blocking event. Also I am not sure if it would work (the transition could happen too fast for the event loop to be triggered)

React Router never seems to have solved this issue and for a lot of us that's the main issue. This is not Next specific.

@agnese-kerubina
Copy link

agnese-kerubina commented Sep 10, 2021

  // prompt the user if they try and leave with unsaved changes
  useEffect(() => {
    const warningText =
      'You have unsaved changes - are you sure you wish to leave this page?';
    const handleWindowClose = (e: BeforeUnloadEvent) => {
      if (!unsavedChanges) return;
      e.preventDefault();
      return (e.returnValue = warningText);
    };
    const handleBrowseAway = () => {
      if (!unsavedChanges) return;
      if (window.confirm(warningText)) return;
      router.events.emit('routeChangeError');
      throw 'routeChange aborted.';
    };
    window.addEventListener('beforeunload', handleWindowClose);
    router.events.on('routeChangeStart', handleBrowseAway);
    return () => {
      window.removeEventListener('beforeunload', handleWindowClose);
      router.events.off('routeChangeStart', handleBrowseAway);
    };
  }, [unsavedChanges]);

If I am not missing something, this is not exactly a perfect solution. If I use router.back() and as documentation states as of now is equivalent of window.history.back(), on an event where previous page is within the app, transition is indeed prevented, but route history changes. Is there a way to prevent this? I've been stuck a while on it.

I could get away with not using router.back(), but window.history.back() is something that can happen.

@uDaniAlves
Copy link

Thanks for that snippet @Fensterbank. Super useful.

I did notice one possible issue with it. Which I think is what prompted you to use the leaveConfirmed variable.

The function gets added as a listener to the event each time the component is rendered. So the page may end up creating hundreds of copies of that function, and calling each one of those copies when a user browses internally. And these never get cleared. Of course that might not be an issue in many use cases. But it was a problem with my particular scenario. Either way, I might suggest adding the router event listener in an event hook, just like the window beforeunload listener.

Here is a version with this approach, for anyone who gets to this page looking for another solution. Note, I have adapted it a bit further for my requirements.

  // prompt the user if they try and leave with unsaved changes
  useEffect(() => {
    const warningText =
      'You have unsaved changes - are you sure you wish to leave this page?';
    const handleWindowClose = (e: BeforeUnloadEvent) => {
      if (!unsavedChanges) return;
      e.preventDefault();
      return (e.returnValue = warningText);
    };
    const handleBrowseAway = () => {
      if (!unsavedChanges) return;
      if (window.confirm(warningText)) return;
      router.events.emit('routeChangeError');
      throw 'routeChange aborted.';
    };
    window.addEventListener('beforeunload', handleWindowClose);
    router.events.on('routeChangeStart', handleBrowseAway);
    return () => {
      window.removeEventListener('beforeunload', handleWindowClose);
      router.events.off('routeChangeStart', handleBrowseAway);
    };
  }, [unsavedChanges]);

So far, it seems to work pretty reliably.

When I click on back button the pathname changes I expected it to be restored if the user clicks on cancel button. Anyone facing this issue?

@anetecima
Copy link

anetecima commented Oct 5, 2021

Thanks for that snippet @Fensterbank. Super useful.
I did notice one possible issue with it. Which I think is what prompted you to use the leaveConfirmed variable.
The function gets added as a listener to the event each time the component is rendered. So the page may end up creating hundreds of copies of that function, and calling each one of those copies when a user browses internally. And these never get cleared. Of course that might not be an issue in many use cases. But it was a problem with my particular scenario. Either way, I might suggest adding the router event listener in an event hook, just like the window beforeunload listener.
Here is a version with this approach, for anyone who gets to this page looking for another solution. Note, I have adapted it a bit further for my requirements.

  // prompt the user if they try and leave with unsaved changes
  useEffect(() => {
    const warningText =
      'You have unsaved changes - are you sure you wish to leave this page?';
    const handleWindowClose = (e: BeforeUnloadEvent) => {
      if (!unsavedChanges) return;
      e.preventDefault();
      return (e.returnValue = warningText);
    };
    const handleBrowseAway = () => {
      if (!unsavedChanges) return;
      if (window.confirm(warningText)) return;
      router.events.emit('routeChangeError');
      throw 'routeChange aborted.';
    };
    window.addEventListener('beforeunload', handleWindowClose);
    router.events.on('routeChangeStart', handleBrowseAway);
    return () => {
      window.removeEventListener('beforeunload', handleWindowClose);
      router.events.off('routeChangeStart', handleBrowseAway);
    };
  }, [unsavedChanges]);

So far, it seems to work pretty reliably.

When I click on back button the pathname changes I expected it to be restored if the user clicks on cancel button. Anyone facing this issue?

I use this a bit differently, but the idea is clear.

        //push state, because browser back action changes link and changes history state
        // but we stay on the same page
        if (Router.asPath !== window.location.pathname) {
            window.history.pushState('', '', Router.asPath)
        }

@uDaniAlves
Copy link

Thanks for that snippet @Fensterbank. Super useful.
I did notice one possible issue with it. Which I think is what prompted you to use the leaveConfirmed variable.
The function gets added as a listener to the event each time the component is rendered. So the page may end up creating hundreds of copies of that function, and calling each one of those copies when a user browses internally. And these never get cleared. Of course that might not be an issue in many use cases. But it was a problem with my particular scenario. Either way, I might suggest adding the router event listener in an event hook, just like the window beforeunload listener.
Here is a version with this approach, for anyone who gets to this page looking for another solution. Note, I have adapted it a bit further for my requirements.

  // prompt the user if they try and leave with unsaved changes
  useEffect(() => {
    const warningText =
      'You have unsaved changes - are you sure you wish to leave this page?';
    const handleWindowClose = (e: BeforeUnloadEvent) => {
      if (!unsavedChanges) return;
      e.preventDefault();
      return (e.returnValue = warningText);
    };
    const handleBrowseAway = () => {
      if (!unsavedChanges) return;
      if (window.confirm(warningText)) return;
      router.events.emit('routeChangeError');
      throw 'routeChange aborted.';
    };
    window.addEventListener('beforeunload', handleWindowClose);
    router.events.on('routeChangeStart', handleBrowseAway);
    return () => {
      window.removeEventListener('beforeunload', handleWindowClose);
      router.events.off('routeChangeStart', handleBrowseAway);
    };
  }, [unsavedChanges]);

So far, it seems to work pretty reliably.

When I click on back button the pathname changes I expected it to be restored if the user clicks on cancel button. Anyone facing this issue?

I use this a bit differently, but the idea is clear.

        //push state, because browser back action changes link and changes history state
        // but we stay on the same page
        if (Router.asPath !== window.location.pathname) {
            window.history.pushState('', '', Router.asPath)
        }

Thank you for the solution, it worked perfectly. However on Safari Mobile it works when a navigate through application links but the back button doesn't work. Did anyone experienced this?

@anetecima
Copy link

Thanks for that snippet @Fensterbank. Super useful.
I did notice one possible issue with it. Which I think is what prompted you to use the leaveConfirmed variable.
The function gets added as a listener to the event each time the component is rendered. So the page may end up creating hundreds of copies of that function, and calling each one of those copies when a user browses internally. And these never get cleared. Of course that might not be an issue in many use cases. But it was a problem with my particular scenario. Either way, I might suggest adding the router event listener in an event hook, just like the window beforeunload listener.
Here is a version with this approach, for anyone who gets to this page looking for another solution. Note, I have adapted it a bit further for my requirements.

  // prompt the user if they try and leave with unsaved changes
  useEffect(() => {
    const warningText =
      'You have unsaved changes - are you sure you wish to leave this page?';
    const handleWindowClose = (e: BeforeUnloadEvent) => {
      if (!unsavedChanges) return;
      e.preventDefault();
      return (e.returnValue = warningText);
    };
    const handleBrowseAway = () => {
      if (!unsavedChanges) return;
      if (window.confirm(warningText)) return;
      router.events.emit('routeChangeError');
      throw 'routeChange aborted.';
    };
    window.addEventListener('beforeunload', handleWindowClose);
    router.events.on('routeChangeStart', handleBrowseAway);
    return () => {
      window.removeEventListener('beforeunload', handleWindowClose);
      router.events.off('routeChangeStart', handleBrowseAway);
    };
  }, [unsavedChanges]);

So far, it seems to work pretty reliably.

When I click on back button the pathname changes I expected it to be restored if the user clicks on cancel button. Anyone facing this issue?

I use this a bit differently, but the idea is clear.

        //push state, because browser back action changes link and changes history state
        // but we stay on the same page
        if (Router.asPath !== window.location.pathname) {
            window.history.pushState('', '', Router.asPath)
        }

Thank you for the solution, it worked perfectly. However on Safari Mobile it works when a navigate through application links but the back button doesn't work. Did anyone experienced this?

you need to call it only when you are blocking the redirect.

@uDaniAlves
Copy link

you need to call it only when you are blocking the redirect.

useEffect(() => {
  const warningText =
	  'You have unsaved changes - are you sure you wish to leave this page?';
  const handleWindowClose = (e: BeforeUnloadEvent) => {
	  if (!unsavedChanges) return;
	  e.preventDefault();
	  return (e.returnValue = warningText);
  };
  const handleBrowseAway = () => {
	  if (!unsavedChanges) return;
	  if (window.confirm(warningText)) return;
	  Router.events.emit('routeChangeError');
	  //push state, because browser back action changes link and changes history state
	  // but we stay on the same page
	  if (Router.asPath !== window.location.pathname) {
		  window.history.pushState('', '', Router.asPath);
	  }
	  throw 'routeChange aborted.';
  };
  window.addEventListener('beforeunload', handleWindowClose);
  Router.events.on('routeChangeStart', handleBrowseAway);
  return () => {
	  window.removeEventListener('beforeunload', handleWindowClose);
	  Router.events.off('routeChangeStart', handleBrowseAway);
  };
}, [unsavedChanges]);

I'm doing like this, however, the back button on safari mobile still does nothing. It's a weird behaviour. Only ios safary works like this.

@rahu-I
Copy link

rahu-I commented Dec 10, 2021

you need to call it only when you are blocking the redirect.

useEffect(() => {
  const warningText =
	  'You have unsaved changes - are you sure you wish to leave this page?';
  const handleWindowClose = (e: BeforeUnloadEvent) => {
	  if (!unsavedChanges) return;
	  e.preventDefault();
	  return (e.returnValue = warningText);
  };
  const handleBrowseAway = () => {
	  if (!unsavedChanges) return;
	  if (window.confirm(warningText)) return;
	  Router.events.emit('routeChangeError');
	  //push state, because browser back action changes link and changes history state
	  // but we stay on the same page
	  if (Router.asPath !== window.location.pathname) {
		  window.history.pushState('', '', Router.asPath);
	  }
	  throw 'routeChange aborted.';
  };
  window.addEventListener('beforeunload', handleWindowClose);
  Router.events.on('routeChangeStart', handleBrowseAway);
  return () => {
	  window.removeEventListener('beforeunload', handleWindowClose);
	  Router.events.off('routeChangeStart', handleBrowseAway);
  };
}, [unsavedChanges]);

I'm doing like this, however, the back button on safari mobile still does nothing. It's a weird behaviour. Only ios safary works like this.

@uDaniAlves Facing the same issue on Safari Mobile. Were you able to get a fix for this?

@balazsorban44
Copy link
Member

This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@vercel vercel locked as resolved and limited conversation to collaborators Jan 27, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.