Muhammad  Awais
Muhammad Awais

Reputation: 513

Next JS: Warn User for Unsaved Form before Route Change

In Next How can i stop Router Navigation in Next JS.

I am trying to use routerChangeStart event to stop navigation.

useEffect(() => {
    const handleRouteChange = (url: string): boolean => {
      if (dirty) { 
        return false;
      }
      return true;
    };

    Router.events.on('routeChangeStart', handleRouteChange);
    return () => {
      Router.events.off('routeChangeStart', handleRouteChange);
    };
  }, []);

Upvotes: 33

Views: 33439

Answers (8)

Manu Benjamin
Manu Benjamin

Reputation: 997

If you are using the next.js app router, router events are not available in the 'next/navigation' to handle the page unload event. You can use the JavaScript window 'beforeunload' event to handle this as a workaround.

const checkForUnsavedData= (event: any) => {     
  if (unsavedData) { // you can use your logic to check any unsaved data
    event.preventDefault();
    // legacy browser support
    event.returnValue = true;
  }
};

//Register beforeunload event in the useEffect hook
useEffect(() => {
  window.addEventListener('beforeunload', checkForUnsavedData);
  return () => {
    window.removeEventListener('beforeunload', checkForUnsavedData);
  };
});

Upvotes: 1

Coder Gautam YT
Coder Gautam YT

Reputation: 2197

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

For your code specifically with the dirty function, you can use this below code as a reference. It is based off the above samples

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

const YourComponent = () => {
  const router = useRouter();
  const [dirty, setDirty] = useState(false);
  const [modalOpen, setModalOpen] = useState(false);
  const [nextPath, setNextPath] = useState("");

  const handleRouteChange = (url) => {
    if (dirty) {
      router.events.off("routeChangeStart", handleRouteChange);
      setModalOpen(true);
      setNextPath(url);
    }
  };

  const proceedNavigation = () => {
    setModalOpen(false);
    router.push(nextPath);
  };

  const cancelNavigation = () => {
    setModalOpen(false);
    setNextPath("");
    router.events.on("routeChangeStart", handleRouteChange);
  };

  useEffect(() => {
    router.events.on("routeChangeStart", handleRouteChange);
    return () => {
      router.events.off("routeChangeStart", handleRouteChange);
    };
  }, [dirty]);

  return (
    <>
      {/* ... your component logic ... */}
      <ConfirmationModal 
        open={modalOpen}
        onProceed={proceedNavigation}
        onCancel={cancelNavigation}
      />
    </>
  );
};

Upvotes: 0

PanosCool
PanosCool

Reputation: 185

The solution below is based on this answer here https://github.com/vercel/next.js/discussions/9662#discussioncomment-4560150

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

const useNavigationPrompt = (isBlocked: boolean, message: string) => {
  const router = useRouter();

// Block next routes
useEffect(() => {
    const beforeHistoryChange = () => {
    if (isBlocked && !window.confirm(message)) {
      // throwing error prevent next router change
      throw "Route change aborted.";
    }
 };

 router.events.on("beforeHistoryChange", beforeHistoryChange);

 return () => {
   router.events.off("beforeHistoryChange", beforeHistoryChange);
 };
}, [isBlocked, message, router.events]);

// Block non-next routes
useEffect(() => {
  const unload = (event: { preventDefault: () => void; returnValue?: 
    string }) => {
  if (isBlocked) {
    event.preventDefault(); // required for mozilla

    event.returnValue = ""; // required for chrome
  }
 };

 window.addEventListener("beforeunload", unload);

 return () => {
    window.removeEventListener("beforeunload", unload);
  };
 }, [isBlocked]);

 return null;
};

export default useNavigationPrompt;

Upvotes: 0

AleXiuS
AleXiuS

Reputation: 830

Thanks @raimohanska for good solution. I did a small update to include confirmation for page reload as well:

/**
 * Asks for confirmation to leave/reload if there are unsaved changes.
 */
import Router from 'next/router';
import { useEffect } from 'react';

export const useOnLeavePageConfirmation = (unsavedChanges: boolean) => {
  useEffect(() => {
    // For reloading.
    window.onbeforeunload = () => {
      if (unsavedChanges) {
        return 'You have unsaved changes. Do you really want to leave?';
      }
    };

    // For changing in-app route.
    if (unsavedChanges) {
      const routeChangeStart = () => {
        const ok = confirm('You have unsaved changes. Do you really want to leave?');
        if (!ok) {
          Router.events.emit('routeChangeError');
          throw 'Abort route change. Please ignore this error.';
        }
      };

      Router.events.on('routeChangeStart', routeChangeStart);
      return () => {
        Router.events.off('routeChangeStart', routeChangeStart);
      };
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [unsavedChanges]);
};

Usage:

useOnLeavePageConfirmation(changesUnsaved);

Upvotes: 4

Erich
Erich

Reputation: 2773

You need to make a hook that will prevent the router from changing. But for it to work correctly, you should know if your form is pristine or not. To do that with react-final-form they have a FormSpy component that can subscribe to that:

import { Form, FormSpy } from 'react-final-form'
import { useWarnIfUnsaved } from '@hooks/useWarnIfUnsaved'

const [isPristine, setPristine] = useState(true)
useWarnIfUnsaved(!isPristine, () => {
  return confirm('Warning! You have unsaved changes.')
})
return (
  <Form
    render={({ handleSubmit, submitting, submitError }) => {
      return (
        <>
          <FormSpy subscription={{ pristine: true }}>
            {(props) => {
              setPristine(props.pristine)
              return undefined
            }}
          </FormSpy>
...

And the suggested hook for Typescript from @raimohanska worked for me:

import Router from "next/router"
import { useEffect } from "react"

export const useWarnIfUnsaved = (unsavedChanges: boolean, callback: () => boolean) => {
  useEffect(() => {
    if (unsavedChanges) {
      const routeChangeStart = () => {
        const ok = callback()
        if (!ok) {
          Router.events.emit("routeChangeError")
          throw "Abort route change. Please ignore this error."
        }
      }
      Router.events.on("routeChangeStart", routeChangeStart)

      return () => {
        Router.events.off("routeChangeStart", routeChangeStart)
      }
    }
  }, [unsavedChanges])
}

Upvotes: 0

raimohanska
raimohanska

Reputation: 3415

Here's my custom hook solution that seems to cut it, written in TypeScript.

import Router from "next/router"
import { useEffect } from "react"

const useWarnIfUnsavedChanges = (unsavedChanges: boolean, callback: () => boolean) => {
  useEffect(() => {
    if (unsavedChanges) {
      const routeChangeStart = () => {
        const ok = callback()
        if (!ok) {
          Router.events.emit("routeChangeError")
          throw "Abort route change. Please ignore this error."
        }
      }
      Router.events.on("routeChangeStart", routeChangeStart)

      return () => {
        Router.events.off("routeChangeStart", routeChangeStart)
      }
    }
  }, [unsavedChanges])
}

You can use it in your component as follows:

useWarnIfUnsavedChanges(changed, () => {
  return confirm("Warning! You have unsaved changes.")
})

Upvotes: 19

Alireza Esfahani
Alireza Esfahani

Reputation: 741

It seems there is no perfect way to this but I handle it with this little trick:

React.useEffect(() => {
  const confirmationMessage = 'Changes you made may not be saved.';
  const beforeUnloadHandler = (e: BeforeUnloadEvent) => {
    (e || window.event).returnValue = confirmationMessage;
    return confirmationMessage; // Gecko + Webkit, Safari, Chrome etc.
  };
  const beforeRouteHandler = (url: string) => {
    if (Router.pathname !== url && !confirm(confirmationMessage)) {
      // to inform NProgress or something ...
      Router.events.emit('routeChangeError');
      // tslint:disable-next-line: no-string-throw
      throw `Route change to "${url}" was aborted (this error can be safely ignored). See https://github.com/zeit/next.js/issues/2476.`;
    }
  };
  if (notSaved) {
    window.addEventListener('beforeunload', beforeUnloadHandler);
    Router.events.on('routeChangeStart', beforeRouteHandler);
  } else {
    window.removeEventListener('beforeunload', beforeUnloadHandler);
    Router.events.off('routeChangeStart', beforeRouteHandler);
  }
  return () => {
    window.removeEventListener('beforeunload', beforeUnloadHandler);
    Router.events.off('routeChangeStart', beforeRouteHandler);
  };
}, [notSaved]);

This code will interrupt changing route (with nextJs Route and also browser refresh / close tab action)

Upvotes: 29

Do Anh Bon
Do Anh Bon

Reputation: 151

You can write a custom hook.

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

const useWarnIfUnsavedChanges = (unsavedChanges, callback) => {
    useEffect(() => {
      const routeChangeStart = url => {
        if (unsavedChanges) {
          Router.events.emit('routeChangeError');
          Router.replace(Router, Router.asPath, { shallow: true });
          throw 'Abort route change. Please ignore this error.';
        }
      };

      Router.events.on('routeChangeStart', routeChangeStart);

      return () => {
        Router.events.off('routeChangeStart', routeChangeStart);
      };
    }, [unsavedChanges]);
};

export default useWarnIfUnsavedChanges;

Take inspiration from: https://github.com/vercel/next.js/discussions/12348#discussioncomment-8089

Upvotes: 7

Related Questions