Reputation: 513
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
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
Reputation: 2197
This is a solution for the pages directory (not tested on app!)
How it works:
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
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
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
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
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
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
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