Determining the moment a user exits a page on Next JS

Is there a way to track when the user exits a Next JS page? I have identified 3 possible ways in which a user might leave a page:

  1. Clicking on a link
  2. Performing an action that triggers router.back, router.push, etc...
  3. Closing the tab (i.e. when beforeunload event is fired)

Detecting when a page is exited can be valuable, for instance, notifying the user if unsaved changes are present.

I am looking for something along the lines of:

router.beforeLeavingPage(() => {
    // my callback
})

Answer №1

Utilizing 'next/router' in a similar manner to the Next.js page for socket disconnection.

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

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

  useEffect(() => {
    const disconnectSocket = () => {
      console.log("Disconnecting socket...");
    };

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

    return () => {
      console.log("Component unmounted...");
      router.events.off("routeChangeStart", disconnectSocket);
    };
  }, []);

  return <>My Page</>;
}

Answer №2

router.beforePopState is an effective method for handling browser back buttons, but it may not work as well for <Link> elements on the page.

If you're looking for an alternative solution, check out this resource: https://github.com/vercel/next.js/issues/2694#issuecomment-732990201

For those seeking another option, here's a modified version of the code snippet mentioned above to address specific requirements:

// Notify users of unsaved changes before leaving the page  
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]);

Based on testing, this approach has proven to be quite reliable.

Alternatively, you can manually add an onClick event handler to every <Link> element yourself.

Answer №3

Although browsers have strict limitations on permissions and features, the following workaround has been found effective:

  • window.confirm: for handling next.js router events
  • beforeunload: for managing browser reloads, tab closures, or navigation away from the page
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

Special thanks to the author of this article

Answer №5

When I was developing this code, I focused on two key things:

  • Determining when the nextjs router would be activated
  • Identifying when specific browser events would occur

To address these concerns, I created a hook that is designed to trigger in response to either the use of the next router or certain browser events such as closing a tab or refreshing the page.

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]);
}

To implement this functionality in your component, simply call the hook with the appropriate arguments:

usePreventUserFromErasingContent(isThereModificationNotSaved);

I have utilized a boolean variable created using useState, which can be updated as needed to control when the hook should be triggered.

Answer №6

If you want to implement a custom event handler in your react page or component, you can use the default web api's eventhandler.

if (process.browser) {
  window.onbeforeunload = () => {
    // Add your callback function here
  }
}

Answer №7

For me, this solution was successful when using next-router with React Functional Components.

  1. Include the router event handler
  2. Add an onBeforeUnload event handler
  3. Ensure they are unloaded when the component is unmounted

Check out the full details here

Answer №8

Handle Changes in Route Intercepting with the NextJS App Router Mode

Check out the demo on: [https://github.com/cgfeel/next.v2/assets/578141/fe1e7f24-054a-4f56-892c-5345ac177a75

You can find the source code at: https://github.com/cgfeel/next.v2/tree/master/routing-file/src/app/leaving/proxy

To implement this Provider in your layout, navigate to the app's root directory using: https://github.com/cgfeel/next.v2/blob/master/routing-file/src/components/proxyProvider/index.tsx

'use client'

import { usePathname, useSearchParams } from "next/navigation";
import Script from "next/script";
import { FC, PropsWithChildren, createContext, useEffect, useState } from "react";

const ProxyContext = createContext<ProxyInstance>([undefined, () => {}]);

const ProxyProvider: FC<PropsWithChildren<{}>> = ({ children }) => {
    const [tips, setTips] = useState<string|undefined>();
    const msg = tips === undefined ? tips : (tips||'Are you sure want to leave this page?');

    const pathname = usePathname();
    const searchParams = useSearchParams();

    const url = [pathname, searchParams].filter(i => i).join('?');
    useEffect(() => {
        setTips(undefined);
    }, [url, setTips]);

    useEffect(() => {
        const handleBeforeUnload = (event: BeforeUnloadEvent) => {
            if (msg === undefined) return msg;
            
            event.preventDefault();
            event.returnValue = msg;
            
            return msg;
        };

        const script = document.getElementById('proxy-script');
        if (script) {
            script.dataset.msg = msg||'';
            script.dataset.href = location.href;
        }

        window.addEventListener("beforeunload", handleBeforeUnload);
        return () => {
            window.removeEventListener("beforeunload", handleBeforeUnload);
        }
    }, [msg]);

    return (
        <ProxyContext.Provider
            value={[msg, setTips]}
        >
            <Script
                strategy="afterInteractive"
                id="proxy-script"
                dangerouslySetInnerHTML={{
                    __html: `(() => {
                        const originalPushState = history.pushState.bind(history);
                        let currentPoint = 0;
                        let point = 0;
                        window.history.pushState = function(state, title, url) {
                            state.point = ++point;
                            currentPoint = point;
                            originalPushState(state, title, url);
                        };
                        const originalReplaceState = history.replaceState.bind(history);
                        window.history.replaceState = function(state, title, url) {
                            state.point = currentPoint;
                            originalReplaceState(state, title, url);
                        };
                        window.addEventListener('popstate', function (event) {
                            const { state: nextState } = event;
                            const isback = currentPoint > nextState.point;

                            currentPoint = nextState.point;

                            const script = document.getElementById('proxy-script');
                            if (!script || location.href === script.dataset.href) return;

                            const msg = script.dataset.msg||'';
                            const confirm = msg == '' ? true : window.confirm(msg);
                            if (!confirm) {
                                event.stopImmediatePropagation();
                                isback ? history.forward() : history.back();
                            }
                        });
                    })()`,
                }}
            ></Script>
            {children}
        </ProxyContext.Provider>
    );
};

export type ProxyInstance = [
    string|undefined, (tips?: string) => void
]

export { ProxyContext };

export default ProxyProvider;

Answer №9

Check out my latest article on Medium about how to Prevent Route Changes and Avoid Data Loss in Next.js by using the code snippet below:

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;
    }

    // Rest of the code remains the same...
    
  }, [shouldPreventLeaving, message, confirmationDialog]);
};

Answer №10

Revamped Solution for 2023

This innovative solution is tailored for the pages directory (not yet tested on app!)

Here's how it operates:

  1. Utilizes router change events to monitor page transitions without refreshing
  2. Makes use of window.onbeforeunload event to detect when a user closes the tab or refreshes the page

Implementation:

Insert this code snippet into your _app.js file. Alternatively, you can place it in a specific page, but it will only function for that particular page. Another option is to create a separate file for this purpose and import it wherever necessary.

  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);
    };
  }, []);

Answer №11

If you're looking to add a before unload confirmation message using React, you can utilize 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]);
};

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is there a way to prevent Express from automatically adding a slash to a route?

Despite my extensive search for a solution, none of them have proven effective. Perhaps you could provide some assistance. I'm currently working on a Node.JS and Express-based plugin webserver. The main code for the webserver is as follows: This sec ...

Steps for inserting a word into an element using jQuery

How can I use JQuery to insert English words into an element? Before: رایجترین نوع این پارامتر کلاس پایه eventArgs می باشد. After : رایجترین نوع این پارامتر کلاس پایه <bdo>eventArgs& ...

Ways to retrieve slider value when button is clicked?

I am currently working on a range-slider that has two ranges and I need to retrieve the slider value in my javascript code. Here is my approach so far: <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.cs ...

The Vue and Element popover feature is unable to modify the current page

After hiding the popover and reopening it, it seems that the value of currentPage remains unchanged. Below is the HTML CODE: <el-popover placement="bottom" trigger="click" title="网段详情" @hide="popoverHide"> <el-table :data="in ...

Testing components in React Native using asynchronous Jest methods

I have a component that triggers a fetch request when it mounts and then displays the results. I've been struggling to create a test snapshot of this component after the request is completed. I've searched on various forums like SO but haven&apo ...

"Is there a way to adjust the range slider to display currency instead of

I stumbled upon this amazing slider on codepen. Can someone guide me on how to adjust it to display a range from €500 to €6000 while keeping the vibrant red background? I've attempted various solutions like: <input id = "range" type = "range ...

What is the best way to create a layout with two images positioned in the center?

Is it possible to align the two pictures to the center of the page horizontally using only HTML and CSS? I've tried using this code but it doesn't seem to work: #product .container { display: flex; justify-content: space-between; flex-w ...

Guide to smoothly scroll to the top of an element containing collapsible panels using Jquery

I've encountered an issue with a series of divs that are set to toggle the state of a collapsible div upon clicking (similar to an accordion widget). The functionality works as intended, but I'm facing a challenge - I want the page to scroll to t ...

Resolve CORS error when uploading images with AJAX and Node.js using FormData

I am incorporating vanilla javascript (AJAX) to submit a form and utilizing Formdata(). The submission of the form is intercepted by nodejs and linked to a database. However, there is an issue that arises when I try to connect with nodejs after adding a f ...

Creating JavaScript object fields with default values in an AngularJS model: A Step-by-Step Guide

As I work on developing the model layer for my AngularJS application, I came across some valuable advice on using functions to create objects. This source emphasizes the use of functions like: function User(firstName, lastName, role, organisation) { // ...

What causes the failure of making an ajax call tied to a class upon loading when dealing with multiple elements?

I can see the attachment in the console, but for some reason, the ajax call never gets triggered. This snippet of HTML code is what I'm using to implement the ajax call: <tr> <td>Sitename1</td> <td class="ajax-delsit ...

Tips for creating animations with JavaScript on a webpage

I created a navigation bar for practice and attempted to show or hide its content only when its title is clicked. I successfully toggled a hidden class on and off, but I also wanted it to transition and slide down instead of just appearing suddenly. Unfort ...

Creating a function in AngularJS to select all checkboxes

I recently started working with Angular and I implemented a select all checkbox that checks all the boxes using ng-model/ng-checked. <th> <input type="checkbox" id="selectAll" ng-model="selectAll"/> </th> <th ...

The onClick event handler fails to trigger in React

Original Source: https://gist.github.com/Schachte/a95efbf7be0c4524d1e9ac2d7e12161c An onClick event is attached to a button. It used to work with an old modal but now, with a new modal, it does not trigger. The problematic line seems to be: <button cla ...

Using $.getJSON is not functioning properly, but including the JSON object directly within the script is effective

I'm currently working on dynamically creating a simple select element where an object's property serves as the option, based on specific constraints. Everything is functioning properly when my JSON data is part of the script. FIDDLE The follow ...

Can a div's style be modified without an id or class attribute using JavaScript or jQuery?

Is it possible to change the style of a div that doesn't have an id or class assigned to it? I need some assistance with this. Here is the div that needs styling: <div style="display:inline-block"> I would like the end result to look somethin ...

The response from a jQuery ajax call to an MVC action method returned empty

I am working on an inventory application with the following layout: <body> <div class="container" style="width: 100%"> <div id="header"> blahblahblah </div> <div class="row"> <div id="rendermenu ...

Trouble arises with the chrome extension code for retrieving tweets from Twitter

I'm currently working on developing a Chrome extension that will display tweets featuring the hashtag perkytweets within the extension's popup. However, I'm facing an issue where nothing is being displayed. Let's take a look at the cod ...

Various input tools available for every Textarea

I'm grappling with this particular case. Each textarea should have its own toolbox, but currently only one is active (I anticipate having more than 2 areas, so JavaScript needs to be able to recognize them by ID) I attempted to use something like: f ...

How can I use query to swap out elements within a div when clicked?

I have a project with two separate div classes named demo-heart and demo-coal. The goal is to implement functionality that allows the user to click on the fa-heart icon and have it switch to fa-coal, and vice versa when clicking on fa-coal. <div class ...