Reputation: 347
I'm using react-router-dom: "^6.2.2"
in my project for a long time, but I don't know before that this version is not included useBlocker()
and usePrompt()
. So I'm found this solution and followed them. Then implemented into React Hook createContext()
and useContext()
. The dialog is displayed when changing route or refresh the page as expected.
But it has an error that useLocation()
get the previous location despite the fact that I'm at the current page.
The NavigationBlocker
code.
import React, { useState, useEffect, useContext, useCallback, createContext } from "react"
import { useLocation, useNavigate, UNSAFE_NavigationContext } from "react-router-dom"
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material"
const navigationBlockerContext = createContext()
function NavigationBlocker(navigationBlockerHandler,canShowDialogPrompt) {
const navigator = useContext(UNSAFE_NavigationContext).navigator
useEffect(()=>{
console.log("useEffect() in NavigationBlocker")
if (!canShowDialogPrompt) return
// For me, this is the dark part of the code
// maybe because I didn't work with React Router 5,
// and it emulates that
const unblock = navigator.block((tx)=>{
const autoUnblockingTx = {
...tx,
retry() {
unblock()
tx.retry()
}
}
navigationBlockerHandler(autoUnblockingTx)
})
return unblock
})
}
function NavigationBlockerController(canShowDialogPrompt) {
// It's look like this function is being re-rendered before routes done that cause the useLocation() get the previous route page.
const navigate = useNavigate();
const currentLocation = useLocation();
const [showDialogPrompt, setShowDialogPrompt] = useState(false);
const [wantToNavigateTo, setWantToNavigateTo] = useState(null);
const [isNavigationConfirmed, setIsNavigationConfirmed] = useState(false);
const handleNavigationBlocking = useCallback(
(locationToNavigateTo) => {
// currentLocation.pathname is the previous route but locationToNavigateTo.location.pathname is the current route
if (!isNavigationConfirmed && locationToNavigateTo.location.pathname !== currentLocation.pathname) // {
setShowDialogPrompt(true);
setWantToNavigateTo(locationToNavigateTo);
return false;
}
return true;
},
[isNavigationConfirmed]
);
const cancelNavigation = useCallback(() => {
setIsNavigationConfirmed(false);
setShowDialogPrompt(false);
}, []);
const confirmNavigation = useCallback(() => {
setIsNavigationConfirmed(true);
setShowDialogPrompt(false);
}, []);
useEffect(() => {
if (isNavigationConfirmed && wantToNavigateTo) {
navigate(wantToNavigateTo.location.pathname);
setIsNavigationConfirmed(false)
setWantToNavigateTo(null)
}
}, [isNavigationConfirmed, wantToNavigateTo]);
NavigationBlocker(handleNavigationBlocking, canShowDialogPrompt);
return [showDialogPrompt, confirmNavigation, cancelNavigation];
}
function LeavingPageDialog({showDialog,setShowDialog,cancelNavigation,confirmNavigation}) {
const preventDialogClose = (event,reason) => {
if (reason) {
return
}
}
const handleConfirmNavigation = () => {
setShowDialog(false)
confirmNavigation()
}
const handleCancelNavigation = () => {
setShowDialog(true)
cancelNavigation()
}
return (
<Dialog fullWidth open={showDialog} onClose={preventDialogClose}>
<DialogTitle>ต้องการบันทึกการเปลี่ยนแปลงหรือไม่</DialogTitle>
<DialogContent>
<DialogContentText>
ดูเหมือนว่ามีการแก้ไขข้อมูลเกิดขึ้น
ถ้าออกจากหน้านี้โดยที่ไม่มีการบันทึกข้อมูล
การเปลี่ยนแปลงทั้งหมดจะสูญหาย
</DialogContentText>
</DialogContent>
<DialogActions>
<Button variant="outlined" color="error" onClick={handleConfirmNavigation}>
ละทิ้งการเปลี่ยนแปลง
</Button>
<Button variant="contained" onClick={handleCancelNavigation}>
กลับไปบันทึกข้อมูล
</Button>
</DialogActions>
</Dialog>
)
}
export function NavigationBlockerProvider({children}) {
const [showDialogLeavingPage,setShowDialogLeavingPage] = useState(false)
const [showDialogPrompt,confirmNavigation,cancelNavigation] = NavigationBlockerController(showDialogLeavingPage)
return (
<navigationBlockerContext.Provider value={{showDialog:setShowDialogLeavingPage}}>
<LeavingPageDialog showDialog={showDialogPrompt} setShowDialog={setShowDialogLeavingPage} cancelNavigation={cancelNavigation} confirmNavigation={confirmNavigation}/>
{children}
</navigationBlockerContext.Provider>
)
}
export const useNavigationBlocker = () => {
return useContext(navigationBlockerContext)
}
Expected comparison.
"/user_profile" === "/user_profile"
Error in comparison code.
"/user_profile" === "/home"
// locationToNavigateTo and currentLocation variable
The NavigationBlocker consumer code usage example.
function UserProfile() {
const prompt = useNavigatorBlocker()
const enablePrompt = () => {
prompt.showDialog(true)
}
const disablePrompt = () => {
prompt.showDialog(false)
}
}
The dialog image if it works correctly and if I click discard change
, then route to the page that I clicked before. (Not pop-up when clicked anything except changing route.)
There is a bug that the dialog is poped-up when clicked at the menu bar button. When I clicked discard change
the page is not changed.
Thank you, any help is appreciated.
Upvotes: 2
Views: 5692
Reputation: 202846
From what I can see your useNavigationBlockerController
hook handleNavigationBlocking
memoized callback is missing a dependency on the location.pathname
value. In other words, it is closing over and referencing a stale value.
Add the missing dependencies:
const navigationBlockerContext = createContext();
...
function useNavigationBlockerHandler(
navigationBlockerHandler,
canShowDialogPrompt
) {
const navigator = useContext(UNSAFE_NavigationContext).navigator;
useEffect(() => {
if (!canShowDialogPrompt) return;
// For me, this is the dark part of the code
// maybe because I didn't work with React Router 5,
// and it emulates that
const unblock = navigator.block((tx) => {
const autoUnblockingTx = {
...tx,
retry() {
unblock();
tx.retry();
}
};
navigationBlockerHandler(autoUnblockingTx);
});
return unblock;
});
}
...
function useNavigationBlockerController(canShowDialogPrompt) {
// It's look like this function is being re-rendered before routes done that cause the useLocation() get the previous route page.
const navigate = useNavigate();
const currentLocation = useLocation();
const [showDialogPrompt, setShowDialogPrompt] = useState(false);
const [wantToNavigateTo, setWantToNavigateTo] = useState(null);
const [isNavigationConfirmed, setIsNavigationConfirmed] = useState(false);
const handleNavigationBlocking = useCallback(
(locationToNavigateTo) => {
// currentLocation.pathname is the previous route but locationToNavigateTo.location.pathname is the current route
if (
!isNavigationConfirmed &&
locationToNavigateTo.location.pathname !== currentLocation.pathname
) {
setShowDialogPrompt(true);
setWantToNavigateTo(locationToNavigateTo);
return false;
}
return true;
},
[isNavigationConfirmed, currentLocation.pathname] // <-- add current pathname
);
const cancelNavigation = useCallback(() => {
setIsNavigationConfirmed(false);
setShowDialogPrompt(false);
}, []);
const confirmNavigation = useCallback(() => {
setIsNavigationConfirmed(true);
setShowDialogPrompt(false);
}, []);
useEffect(() => {
if (isNavigationConfirmed && wantToNavigateTo) {
navigate(wantToNavigateTo.location.pathname);
setIsNavigationConfirmed(false);
setWantToNavigateTo(null);
}
}, [isNavigationConfirmed, navigate, wantToNavigateTo]); // <-- add navigate
useNavigationBlockerHandler(handleNavigationBlocking, canShowDialogPrompt);
return [showDialogPrompt, confirmNavigation, cancelNavigation];
}
...
export function NavigationBlockerProvider({ children }) {
const [showDialogLeavingPage, setShowDialogLeavingPage] = useState(false);
const [
showDialogPrompt,
confirmNavigation,
cancelNavigation
] = useNavigationBlockerController(showDialogLeavingPage);
return (
<navigationBlockerContext.Provider
value={{ showDialog: setShowDialogLeavingPage }}
>
<LeavingPageDialog
showDialog={showDialogPrompt}
setShowDialog={setShowDialogLeavingPage}
cancelNavigation={cancelNavigation}
confirmNavigation={confirmNavigation}
/>
{children}
</navigationBlockerContext.Provider>
);
}
...
export const useNavigationBlocker = () => {
return useContext(navigationBlockerContext);
};
Upvotes: 1