React Router useLocation() 位置不跟随到当前页面

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

我在我的项目中使用了很长时间的react-router-dom: "^6.2.2",但我之前不知道这个版本不包括useBlocker()usePrompt()。所以我找到了这个 solution 并关注了他们。然后实现到 React Hook createContext()useContext()。更改路线或按预期刷新页面时会显示该对话框。 但是它有一个错误,尽管我在当前页面,useLocation() 得到了以前的位置。

NavigationBlocker代码。

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

预期比较。

"/user_profile" === "/user_profile"

比较代码错误。

"/user_profile" === "/home" 
// locationToNavigateTo and currentLocation variable

NavigationBlocker 消费者代码使用示例。


function UserProfile() {
  const prompt = useNavigatorBlocker()
  
  const enablePrompt = () => {
    prompt.showDialog(true)
  }

  const disablePrompt = () => {
    prompt.showDialog(false)
  }
}

对话框图像,如果它工作正常并且如果我单击 discard change,则路由到我之前单击的页面。 (除了更改路线外,单击任何内容时都不会弹出。)

存在点击菜单栏按钮时弹出对话框的错误。当我点击 discard change 页面没有改变。

谢谢,如有任何帮助,我们将不胜感激。

据我所知,您的 useNavigationBlockerController 挂钩 handleNavigationBlocking 记忆回调缺少对 location.pathname 值的依赖。换句话说,它正在关闭并引用过时的值。

添加缺少的依赖项:

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