During my work, I was asked to add a feature that pops up a dialogue and asks user if they would like to save their unsaved form as a draft when they are are leaving/refresh/close the page. As I started the task, I find it’s bit tricky. I did a lot a research on it and I think it’s worth to have a note here. The app uses React 18 along with react-router-dom v6.6
Task description
When a user input at least one field in the form, a dialogue should popup up on in the following scenario:
-
The user is trying to refresh the page
-
The user is trying to close the tab
-
The user is trying to navigate to any other page
For the first and second cases, I decided to handled in the beforeunload
event. For the third case, I decide to handle it using useBlock
, a hook provided by react-router-dom
. But I encountered issues.
Beforeunload event
This event is fired when the window, the document and its resources are about to be unloaded. The document is still visible and the event is still cancelable at this point.Window: beforeunload event MDN. If you have the following codes, it will pop up a confirm window to ask if you want to leave even if the page may have unsaved data. (See attached screenshots)
const beforeUnloadListener = (event) => {
event.preventDefault();
return event.returnValue = '';
};
const nameInput = document.querySelector("#name");
nameInput.addEventListener("input", (event) => {
if (event.target.value !== "") {
addEventListener("beforeunload", beforeUnloadListener, {capture: true});
} else {
removeEventListener("beforeunload", beforeUnloadListener, {capture: true});
}
});
However, the beforeunload event behaviour differs from different browsers. Some browsers, like Chrome will ignore window.confirm
. It make it impossible to open a dialog to ask users. If I set a setTimeout function, due to the setTimeout() is an asynchronous function, the result of the opened confirm dialogue is inaccessible.
Block navigation
Block navigation is easy if you are using react-router-dom v5
. You just need to use the hooks it provides: usePrompt
and useBlock
. It’s tricky when you are using v6 because they remove them during v6-6.6. To solve this, you have four options:
- Manually adding useBlock
- Upgrade to v6.8 (They add new unstable useBlock)
- Downgrade to v5.
- Figure out a way without using react-router-dom
Manually adding useBlock
To manually add the hook back, you have to replace BrowserRouter
with unstable_HistoryRouter
, install history
package, and inject it into unstable_HistoryRouter
. Otherwise, it will throw an error: navigator.block
is not a function (Because react-router-dom
v6.6 streamlined and inlined the history
library and removed the block method).
This method works but it ships with the risk of using unstable_HistoryRouter
Here are some useful links gives more details:
use-blocker in react-router doc
issue: Getting usePrompt and useBlocker back in the router #8139
Prompt is not supported in v6
Upgrade to v6.8
To use the unstable useBlock, you have to upgrade v6.8. However, this useBlock is depending on data router. So you have to replace BrowserRouter
with createBrowserRouter
.
Since our app has a complex router, it will largely impact the codebase.
Downgrade to v5
In this solution, you can use usePrompt
directly but since the codebase using new changes in v6, it also requires a lot refactoring.
Other ways but don’t work
-
Try to use
beforeunload
event
This actually doesn’t work. When changing navigation with react-router-dom because we use<Link/>
or<NavLink/>
so that the URL of the current page is updated using the HTML5history.pushState()
orhistory.replaceState()
methods. These methods update the browser’s history stack without actually navigating to a new page, so thebeforeunload
event is not triggered. So this doesn’t work. -
Try
popstate
event
Therotically, the popstate event only fires when the user clicks the back or forward buttons. There is no event for when the programmer calledwindow.history.pushState
orwindow.history.replaceState
according to react-router doc React-router-dom handles it through the history package. And the v6.6 streamlined and inlined the history library and removed the block method.
To make block possible, I’ll have to create my own history instance, which leads to the the Manually adding useBlock approach.
window.addEventListener('popstate', (event) => {
console.log(`${JSON.stringify(event.state)}`);
});
Useful reference
How to block user from leaving a page on a single page app Manually add useBlock and usePrompt history package react-router-dom