Reactjs 在React Router v6中,如何在离开页面/路由之前检查表单是否脏

Reactjs 在React Router v6中,如何在离开页面/路由之前检查表单是否脏,reactjs,react-redux,react-router,material-ui,Reactjs,React Redux,React Router,Material Ui,下面是我正在使用的软件包版本 React version - 16.13.1 react-router-dom version - 6.0.0-beta.0 react-redux version 7.2.0 Material UI version 4.11.0 当用户试图离开当前页面时,如何/什么是检查表单是否已更改的最佳方法?如果表单为dirty,我想提示“您确定要离开…” 我将从useffect()中获取数据,并使用redux reducer呈现UI 我是否应该声明一个变量来保留原始获取

下面是我正在使用的软件包版本

React version - 16.13.1
react-router-dom version - 6.0.0-beta.0
react-redux version 7.2.0
Material UI version 4.11.0
当用户试图离开当前页面时,如何/什么是检查表单是否已更改的最佳方法?如果表单
为dirty
,我想提示“您确定要离开…”

我将从
useffect()
中获取数据,并使用redux reducer呈现UI

我是否应该声明一个变量来保留原始获取的数据以进行脏检查

这就是我正在做的,但它不能正常工作

component.js

 useEffect(() => {
    props.fetchUserInfo();
 })
action.js

export function fetchUserInfo() {
 return (dispatch) => {
     dispatch({type: USER_INITIALSTATE, {Name: 'abc', Age: 20}} 
     )
 }
}
userReducer.js

const initialState = {
  processing: false,
  success: false,
  fail: false,
  Profile: {}
}
let oriState;
let State;
const UserReducer = (state = initialState, action) => {
  if (action.type === USER_INITIALSTATE) {
    oriState = {Profile: action.data};
    State = {...state, Profile: action.data};
    return {...state, Profile: action.data};
  } else if (action.type === OTHERS_ACTION) {
     //update field change
     return {...state, xxx}
  }
}
export const userIsDirty = state => {
  if (oriState && State) {
    return JSON.stringify(oriState.Profile) !== JSON.stringify(State.Profile);
  }
  return false;
};
export default UserReducer;
因此,在我的组件中,我调用
userIsDirty
返回isDirty布尔值,但我还没有弄清楚如何捕获离开页面事件并将其用作触发器来执行脏表单检查

那么如何检测离开当前页面?我在useEffect返回(组件umount)上尝试了一些东西,但是props没有得到更新的INITIALSTATE状态(意味着我将得到Profile:{}),因为它只运行一次,但是如果我添加useEffect可选数组参数,我会得到一个无限循环(可能我设置错了?)

我这样做对吗?我错过了什么?是否有更好/正确的解决方案来实现我的目标

更新 谢谢@gdh,
useBlocker
是我想要的。我用它弹出一个确认对话框

我将分享我完整的代码沙盒,我相信这对将来的人可能会有帮助


看来您正在寻找合适的人选

请仔细阅读,因为并非所有浏览器都符合
event.preventDefault()

在事件处理程序中,您可以执行所需的检查,并根据需要调用阻止窗口关闭


希望这有帮助。

此答案使用路由器v6

  • 您可以使用。
    • 当您转到另一条路线,即装载时,usePrompt将显示确认模式/弹出窗口
    • 当您尝试关闭浏览器时,会出现一个带有消息的常规警报。它在内部卸载之前进行处理
    usePrompt(“您好,来自usePrompt——您确定要离开吗?”,isBlocking);
    
  • 您可以使用
    • useBlocker将在尝试导航离开时(即卸载时)简单地阻止用户
    • 当您尝试关闭浏览器时,会出现一个带有消息的常规警报。它在内部卸载之前进行处理
    useBlocker(
    ()=>“你好,来自useBlocker--你确定要离开吗?”,
    ISBLOCK
    );
    

  • 您也可以使用。但你必须按照自己的逻辑行事 为希望自定义UI
    弹出框/模式框
    而不是浏览器的
    默认提示
    的用户发布此消息,他们正在使用
    react router(v4)
    历史
    。 您可以使用
    自定义历史记录
    并配置
    路由器

    import createBrowserHistory from 'history/createBrowserHistory'
    export const history = createBrowserHistory()
    
    ... 
    import { history } from 'path/to/history';
    
    <Router history={history}>
      <App/>
    </Router>
    
    import { history } from 'path/to/history';
    
    class MyCustomPrompt extends React.Component {
       componentDidMount() {
          this.unblock = history.block(targetLocation => {
               // take your action here     
               return false;
          });
       }
       componentWillUnmount() {
          this.unblock();
       }
       render() {
          //component render here
       }
    }
    

    将此
    MyCustomPrompt
    添加到您想要阻止导航的组件中。

    我遇到了同样的情况,即尝试使用自定义的“令人愉快的”UI确认对话框与react router v6 beta版的
    useBlocker
    挂钩集成,用于在当前路由的表单有未保存的修改时阻止路由转换。我从这个问题底部
    UPDATED
    部分链接的代码开始。我发现这个定制钩子实现并不能满足我的所有需求,所以我对它进行了调整,以支持一个可选的正则表达式参数来定义一组不应该被阻止的路由。同样值得注意的是,codesandbox实现从传入
    useBlocker
    的回调返回一个布尔值,但我发现这没有效果或用处,所以我删除了它。下面是我对修改后的自定义钩子的完整TypeScript实现:

    useNavigationWarning.ts

    import { useState, useEffect, useCallback } from 'react';
    import { useBlocker, useNavigate, useLocation } from 'react-router-dom';
    import { Blocker } from 'history';
    
    export function useNavigationWarning(
      when: boolean,
      exceptPathsMatching?: RegExp
    ) {
      const navigate = useNavigate();
      const location = useLocation();
      const [showPrompt, setShowPrompt] = useState<boolean>(false);
      const [lastLocation, setLastLocation] = useState<any>(null);
      const [confirmedNavigation, setConfirmedNavigation] = useState<boolean>(
        false
      );
    
      const cancelNavigation = useCallback(() => {
        setShowPrompt(false);
      }, []);
    
      const handleBlockedNavigation = useCallback<Blocker>(
        nextLocation => {
          const shouldIgnorePathChange = exceptPathsMatching?.test(
            nextLocation.location.pathname
          );
          if (
            !(confirmedNavigation || shouldIgnorePathChange) &&
            nextLocation.location.pathname !== location.pathname
          ) {
            setShowPrompt(true);
            setLastLocation(nextLocation);
          } else if (shouldIgnorePathChange) {
            // to cancel blocking based on the route we need to retry the nextLocation
            nextLocation.retry();
          }
        },
        [confirmedNavigation, location.pathname, exceptPathsMatching]
      );
    
      const confirmNavigation = useCallback(() => {
        setShowPrompt(false);
        setConfirmedNavigation(true);
      }, []);
    
      useEffect(() => {
        if (confirmedNavigation && lastLocation?.location) {
          navigate(lastLocation.location.pathname);
          // Reset hook state
          setConfirmedNavigation(false);
          setLastLocation(null);
        }
      }, [confirmedNavigation, lastLocation, navigate]);
    
      useBlocker(handleBlockedNavigation, when);
    
      return [showPrompt, confirmNavigation, cancelNavigation] as const;
    }
    
    
    从'react'导入{useState,useffect,useCallback};
    从“react router dom”导入{useBlocker、useNavigate、useLocation};
    从“历史”导入{Blocker};
    导出函数useNavigationWarning(
    当:布尔,
    例外路径匹配?:RegExp
    ) {
    const-navigate=useNavigate();
    const location=useLocation();
    const[showPrompt,setShowPrompt]=useState(false);
    常量[lastLocation,setLastLocation]=useState(null);
    const[confirmedNavigation,setConfirmedNavigation]=使用状态(
    假的
    );
    const cancelNavigation=useCallback(()=>{
    设置显示提示(假);
    }, []);
    const handleBlockedNavigation=useCallback(
    nextLocation=>{
    const shouldinogrepathchange=异常路径匹配?测试(
    nextLocation.location.pathname
    );
    如果(
    !(确认航行| |应忽略更改)&&
    nextLocation.location.pathname!==location.pathname
    ) {
    设置显示提示(true);
    setLastLocation(下一个位置);
    }否则,如果(应忽略准备更改){
    //要根据路由取消阻塞,我们需要重试下一个位置
    nextLocation.retry();
    }
    },
    [确认导航,location.pathname,例外路径匹配]
    );
    const confirmNavigation=useCallback(()=>{
    设置显示提示(假);
    设置确认导航(真);
    }, []);
    useffect(()=>{
    if(确认导航和最后位置?.location){
    导航(lastLocation.location.pathname);
    //重设挂钩状态
    设置确认导航(假);
    setLastLocation(空);
    }
    },[确认导航、最后定位、导航];
    使用拦截器(手柄锁定导航,何时);
    返回[showPrompt,confirmNavigation,cancelNavigation]作为常量;
    }
    
    @Devb您的问题和更新非常有用,节省了我很多时间。非常感谢。根据您的代码创建了一个HOC。可能对某人有用。 包裹组件上的道具:

    • setPreventNavigation-设置何时阻止导航

    • provideLeaveHandler-集合t
      import { useState, useEffect, useCallback } from 'react';
      import { useBlocker, useNavigate, useLocation } from 'react-router-dom';
      import { Blocker } from 'history';
      
      export function useNavigationWarning(
        when: boolean,
        exceptPathsMatching?: RegExp
      ) {
        const navigate = useNavigate();
        const location = useLocation();
        const [showPrompt, setShowPrompt] = useState<boolean>(false);
        const [lastLocation, setLastLocation] = useState<any>(null);
        const [confirmedNavigation, setConfirmedNavigation] = useState<boolean>(
          false
        );
      
        const cancelNavigation = useCallback(() => {
          setShowPrompt(false);
        }, []);
      
        const handleBlockedNavigation = useCallback<Blocker>(
          nextLocation => {
            const shouldIgnorePathChange = exceptPathsMatching?.test(
              nextLocation.location.pathname
            );
            if (
              !(confirmedNavigation || shouldIgnorePathChange) &&
              nextLocation.location.pathname !== location.pathname
            ) {
              setShowPrompt(true);
              setLastLocation(nextLocation);
            } else if (shouldIgnorePathChange) {
              // to cancel blocking based on the route we need to retry the nextLocation
              nextLocation.retry();
            }
          },
          [confirmedNavigation, location.pathname, exceptPathsMatching]
        );
      
        const confirmNavigation = useCallback(() => {
          setShowPrompt(false);
          setConfirmedNavigation(true);
        }, []);
      
        useEffect(() => {
          if (confirmedNavigation && lastLocation?.location) {
            navigate(lastLocation.location.pathname);
            // Reset hook state
            setConfirmedNavigation(false);
            setLastLocation(null);
          }
        }, [confirmedNavigation, lastLocation, navigate]);
      
        useBlocker(handleBlockedNavigation, when);
      
        return [showPrompt, confirmNavigation, cancelNavigation] as const;
      }
      
      
       import React, { useEffect, useState, useCallback } from 'react'
       import { useNavigate, useBlocker, useLocation } from 'react-router-dom'
      
       export default function withPreventNavigation(WrappedComponent) {
         return function preventNavigation(props) {
           const navigate = useNavigate()
           const location = useLocation()
           const [lastLocation, setLastLocation] = useState(null)
           const [confirmedNavigation, setConfirmedNavigation] = useState(false)
           const [shouldBlock, setShouldBlock] = useState(false)
      
          let handleLeave = null
      
           const cancelNavigation = useCallback(() => {
             setshouldBlock(false)
           },[])
      
           const handleBlockedNavigation = useCallback(
             nextLocation => {
               if (
                 !confirmedNavigation &&
                 nextLocation.location.pathname !== location.pathname
               ) {
                 handleLeave(nextLocation)
                 setLastLocation(nextLocation)
                 return false
               }
               return true
             },
             [confirmedNavigation]
           )
      
           const confirmNavigation = useCallback(() => {
             setConfirmedNavigation(true)
           }, [])
      
           useEffect(() => {
             if (confirmedNavigation && lastLocation) {
               navigate(lastLocation.location.pathname)
             }
           }, [confirmedNavigation, lastLocation])
      
           const provideLeaveHandler = handler => {
             handleLeave = handler
           }
      
           useBlocker(handleBlockedNavigation, shouldBlock)
      
           return (
             <WrappedComponent
               {...props}
               provideLeaveHandler={provideLeaveHandler}
               setPreventNavigation={setShouldBlock}
               confirmNavigation={confirmNavigation}
               cancelNavigation={cancelNavigation} />
           )
         }
       }