13

I would like to detect when the user leaves the page Next JS. I count 3 ways of leaving a page:

  1. by clicking on a link
  2. by doing an action that triggers router.back, router.push, etc...
  3. by closing the tab (i.e. when beforeunload event is fired

Being able to detect when a page is leaved is very helpful for example, alerting the user some changes have not been saved yet.

I would like something like:

router.beforeLeavingPage(() => {
    // my callback
})
Chukwuemeka Maduekwe
  • 6,687
  • 5
  • 44
  • 67
  • You can use the `beforePopState` event to detect changes to the session history navigation as suggested in [Want to have an event handler for the browser's back button with next.js](https://stackoverflow.com/questions/61932918/want-to-have-an-event-handler-for-the-browsers-back-button-with-next-js). This covers both your point **1.** and **2.**. – juliomalves Jan 25 '22 at 00:07

10 Answers10

13

I use 'next/router' like Next.js page to disconnect a socket

import { useEffect } from "react";
import { useRouter } from "next/router";

export default function MyPage() {
  const router = useRouter();

  useEffect(() => {
    const exitingFunction = () => {
      console.log("exiting...");
    };

    router.events.on("routeChangeStart", exitingFunction);

    return () => {
      console.log("unmounting component...");
      router.events.off("routeChangeStart", exitingFunction);
    };
  }, []);

  return <>My Page</>;
}
double-beep
  • 5,031
  • 17
  • 33
  • 41
Luis Moreno
  • 615
  • 6
  • 6
8

router.beforePopState is great for browser back button but not for <Link>s on the page.

Solution found here: https://github.com/vercel/next.js/issues/2694#issuecomment-732990201

... 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.

Alternatively you can add an onClick to all the <Link>s yourself.

700 Software
  • 85,281
  • 83
  • 234
  • 341
6

Browsers heavily restrict permissions and features but this works:

  • window.confirm: for next.js router event
  • beforeunload: for broswer reload, closing tab or navigating away
import { useRouter } from 'next/router'

const MyComponent = () => {
  const router = useRouter()
  const unsavedChanges = true
  const warningText =
    'You have unsaved changes - are you sure you wish to leave this page?'

  useEffect(() => {
    const handleWindowClose = (e) => {
      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])

}
export default MyComponent

Credit to this article

rottitime
  • 1,653
  • 17
  • 29
4

You can use router.beforePopState check here for examples

Cyber Progs
  • 3,656
  • 3
  • 30
  • 39
  • 1
    Here's a practical example on how to use the `beforePopState` event to detect back/forward actions: [Want to have an event handler for the browser's back button with next.js](https://stackoverflow.com/a/69569682/1870780). – juliomalves Dec 03 '21 at 21:29
4

I saw two things when coding it :

  • Knowing when nextjs router would be activated
  • Knowing when specific browser event would happen

I did a hook that way. It triggers if next router is used, or if there is a classic browser event (closing tab, refreshing)

import SingletonRouter, { Router } from 'next/router';

export function usePreventUserFromErasingContent(shouldPreventLeaving) {
  const stringToDisplay = 'Do you want to save before leaving the page ?';

  useEffect(() => {
    // Prevents tab quit / tab refresh
    if (shouldPreventLeaving) {
      // Adding window alert if the shop quits without saving
      window.onbeforeunload = function () {
        return stringToDisplay;
      };
    } else {
      window.onbeforeunload = () => {};
    }

    if (shouldPreventLeaving) {
      // Prevents next routing
      SingletonRouter.router.change = (...args) => {
        if (confirm(stringToDisplay)) {
          return Router.prototype.change.apply(SingletonRouter.router, args);
        } else {
          return new Promise((resolve, reject) => resolve(false));
        }
      };
    }
    return () => {
      delete SingletonRouter.router.change;
    };
  }, [shouldPreventLeaving]);
}

You just have to call your hook in the component you want to cover :

usePreventUserFromErasingContent(isThereModificationNotSaved);

This a boolean I created with useState and edit when needed. This way, it only triggers when needed.

Yoann Buzenet
  • 651
  • 2
  • 5
  • 12
  • This works in my use case though an error will thrown for .change method in Typescript bec. SingletonRouter.router.change is a private method inside next/router – Jeffox Oct 06 '22 at 04:07
3

You can use default web api's eventhandler in your react page or component.

if (process.browser) {
  window.onbeforeunload = () => {
    // your callback
  }
}
Darryl RN
  • 7,432
  • 4
  • 26
  • 46
2

this worked for me in next-router / react-FC

  1. add router event handler
  2. add onBeforeUnload event handler
  3. unload them when component unmounted

https://github.com/vercel/next.js/issues/2476#issuecomment-563190607

blue-hope
  • 2,735
  • 1
  • 9
  • 14
0

I wrote a medium article Prevent Route Changes and Unsaved Data Loss in Next.js. Here is the full code:

import SingletonRouter, { Router } from 'next/router';
import { useEffect } from 'react';

const defaultConfirmationDialog = async (msg?: string) => window.confirm(msg);

/**
 * React Hook
 */
export const useLeavePageConfirmation = (
  shouldPreventLeaving: boolean,
  message: string = 'Changes you made may not be saved.',
  confirmationDialog: (msg?: string) => Promise<boolean> = defaultConfirmationDialog
) => {
  useEffect(() => {
    // @ts-ignore because "change" is private in Next.js
    if (!SingletonRouter.router?.change) {
      return;
    }

    // @ts-ignore because "change" is private in Next.js
    const originalChangeFunction = SingletonRouter.router.change;
    const originalOnBeforeUnloadFunction = window.onbeforeunload;

    /*
     * Modifying the window.onbeforeunload event stops the browser tab/window from
     * being closed or refreshed. Since it is not possible to alter the close or reload
     * alert message, an empty string is passed to trigger the alert and avoid confusion
     * about the option to modify the message.
     */
    if (shouldPreventLeaving) {
      window.onbeforeunload = () => '';
    } else {
      window.onbeforeunload = originalOnBeforeUnloadFunction;
    }

    /*
     * Overriding the router.change function blocks Next.js route navigations
     * and disables the browser's back and forward buttons. This opens up the
     * possibility to use the window.confirm alert instead.
     */
    if (shouldPreventLeaving) {
      // @ts-ignore because "change" is private in Next.js
      SingletonRouter.router.change = async (...args) => {
        const [historyMethod, , as] = args;
        // @ts-ignore because "state" is private in Next.js
        const currentUrl = SingletonRouter.router?.state.asPath.split('?')[0];
        const changedUrl = as.split('?')[0];
        const hasNavigatedAwayFromPage = currentUrl !== changedUrl;
        const wasBackOrForwardBrowserButtonClicked = historyMethod === 'replaceState';
        let confirmed = false;

        if (hasNavigatedAwayFromPage) {
          confirmed = await confirmationDialog(message);
        }

        if (confirmed) {
          // @ts-ignore because "change" is private in Next.js
          Router.prototype.change.apply(SingletonRouter.router, args);
        } else if (wasBackOrForwardBrowserButtonClicked && hasNavigatedAwayFromPage) {
          /*
           * The URL changes even if the user clicks "false" to navigate away from the page.
           * It is necessary to update it to reflect the current URL.
           */
          // @ts-ignore because "state" is private in Next.js
          await SingletonRouter.router?.push(SingletonRouter.router?.state.asPath);

          /*
           * @todo
           *   I attempted to determine if the user clicked the forward or back button on the browser,
           *   but was unable to find a solution after several hours of effort. As a result, I temporarily
           *   hardcoded it to assume the back button was clicked, since that is the most common scenario.
           *   However, this may cause issues with the URL if the forward button is actually clicked.
           *   I hope that a solution can be found in the future.
           */
          const browserDirection = 'back';

          browserDirection === 'back'
            ? history.go(1) // back button
            : history.go(-1); // forward button
        }
      };
    }

    /*
     * When the component is unmounted, the original change function is assigned back.
     */
    return () => {
      // @ts-ignore because "change" is private in Next.js
      SingletonRouter.router.change = originalChangeFunction;
      window.onbeforeunload = originalOnBeforeUnloadFunction;
    };
  }, [shouldPreventLeaving, message, confirmationDialog]);
};
codeBelt
  • 1,727
  • 16
  • 22
0

updated solution 2023

This is a solution for the pages directory (not tested on app!)

How it works:

  1. Uses router change events - to track when changing page without refresh
  2. Uses window.onbeforeunload event - to track when user closed the tab or refreshed the page

Code:

Please use this code in the _app.js file. You can also put it in a specific page but then it won't execute on all the pages. You can also make a seperate file for this and import it wherever needed.

  useEffect(() => {
    const exitingFunction = async () => {
      console.log("exiting...");
    };

    router.events.on("routeChangeStart", exitingFunction);
    window.onbeforeunload = exitingFunction;

    return () => {
      console.log("unmounting component...");
      router.events.off("routeChangeStart", exitingFunction);
    };
  }, []);
Coder Gautam YT
  • 1,044
  • 7
  • 20
-1

You can use the react-use npm package

import { useEffect } from "react";
import Router from "next/router";
import { useBeforeUnload } from "react-use";

export const useLeavePageConfirm = (
  isConfirm = true,
  message = "Are you sure want to leave this page?"
) => {
  useBeforeUnload(isConfirm, message);

  useEffect(() => {
    const handler = () => {
      if (isConfirm && !window.confirm(message)) {
        throw "Route Canceled";
      }
    };

    Router.events.on("routeChangeStart", handler);

    return () => {
      Router.events.off("routeChangeStart", handler);
    };
  }, [isConfirm, message]);
};
Embedded_Mugs
  • 2,132
  • 3
  • 21
  • 33