Paweenwat Maneechai
Paweenwat Maneechai

Reputation: 347

React Router useLocation() location is not followed to the current page

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.) The dialog image if it works correctly (Not pop-up when pressed 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. 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

Answers (1)

Drew Reese
Drew Reese

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

Edit react-router-uselocation-location-is-not-followed-to-the-current-page

Upvotes: 1

Related Questions