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

Posted

技术标签:

【中文标题】在 React Router v6 中,如何在离开页面/路由之前检查表单是不是脏【英文标题】:In React Router v6, how to check form is dirty before leaving page/route在 React Router v6 中,如何在离开页面/路由之前检查表单是否脏 【发布时间】:2020-10-28 17:15:52 【问题描述】:

以下是我正在使用的包版本。

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

当用户试图离开当前页面时,如何/什么是检查表单 isDirty(已更改)的最佳方法?如果表单为isDirty,我想提示“您确定要离开...”。

我将从useEffect() 中获取数据并使用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 return(component umount) 上尝试了一些东西,但道具没有获得更新的 INITIALSTATE 状态(意味着我将获得 Profile: ),因为它只运行一次,但如果我添加 useEffect 可选数组参数,我得到一个无限循环(也许我设置错了?)。

useEffect(() => 
    props.fetchUserInfo();
    return () => 
      console.log(props); //not getting initial state object
    ;
  , []);

我这样做是否正确?我错过了什么?有没有更好/正确的解决方案来实现我想要的?

更新

谢谢@gdh,useBlocker 是我想要的。我正在使用它来弹出一个确认对话框。

我将分享我的完整代码框,我相信这可能对将来的某人有所帮助。

show confirmation dialog by using useBlocker

【问题讨论】:

如果我对问题的理解正确,您正在尝试检查用户是否更改了表单数据,并在用户尝试退出页面时显示确认消息,对吧? @ManuSharma 是的,我想做与codesandbox.io/s/… 类似的事情,但这是使用 react-router v5 ......我想使用 v6 方法。基于this document(github.com/ReactTraining/react-router/blob/dev/docs/…)我看到有一个钩子调用useBlocker,但我不知道如何实现它。 【参考方案1】:

此答案使用路由器 v6。

    您可以使用usePrompt。
usePrompt 将在您前往另一条路线(即在山上)时显示确认模式/弹出窗口。 当您尝试关闭浏览器时带有消息的一般警报。它在内部处理 beforeunload
usePrompt("Hello from usePrompt -- Are you sure you want to leave?", isBlocking);
    您可以使用useBlocker
useBlocker 将在尝试导航离开时(即卸载时)简单地阻止用户 当您尝试关闭浏览器时带有消息的一般警报。它在内部处理 beforeunload
useBlocker(
    () => "Hello from useBlocker -- are you sure you want to leave?",
    isBlocking
  );

Demo for both 1 & 2

    您也可以使用beforeunload。但是你必须做你自己的逻辑。 See an example here

【讨论】:

在 React-Router v6.0.0-beta.7 发布后以及当前官方发布的 v6.0.2 中,usePrompt 和 useBlocker 已被删除。他们将来会重新审视这些钩子/类似的东西。 这些都不再适用于 react router v6 啊,是的,他们在 Beta 版中删除了那些钩子,但在 6.0.0 alpha 版中有它们 - 建议我们现在将它们添加到我们自己的项目中(所以我做了一个要点以使其更容易) - gist.github.com/rmorse/426ffcc579922a82749934826fa9f743 @rmorse 不幸的是,似乎 UNSAFE_NavigationContext 也已从 react-router-dom 中删除 哎呀,5天前在一个新版本中......我还没有更新/测试,但我没有看到它被弃用......如果我看一下代码库,它似乎是还有:github.com/remix-run/react-router/…【参考方案2】:

只需为 React Router v6 用户添加一个额外的答案。

从 v6.0.0-beta 开始 - useBlocker 和 usePrompt 已被删除 (to be added back in at a later date)。

建议如果我们在 v6.0.2(撰写本文时的当前版本)中需要它们,我们应该使用现有代码作为示例。

Here is the code directly from the the alpha for these hooks.

因此,要重新添加挂钩将是以下代码(在您的应用程序中的任何位置供使用): ** 我只复制了 react-router-dom 的代码 - 如果您使用的是本机,那么您需要检查上面的链接以获取其他 usePrompt 钩子

/**
 * These hooks re-implement the now removed useBlocker and usePrompt hooks in 'react-router-dom'.
 * Thanks for the idea @piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-953816315
 * Source: https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874#diff-b60f1a2d4276b2a605c05e19816634111de2e8a4186fe9dd7de8e344b65ed4d3L344-L381
 */
import  useContext, useEffect, useCallback  from 'react';
import  UNSAFE_NavigationContext as NavigationContext  from 'react-router-dom';
/**
 * Blocks all navigation attempts. This is useful for preventing the page from
 * changing until some condition is met, like saving form data.
 *
 * @param  blocker
 * @param  when
 * @see https://reactrouter.com/api/useBlocker
 */
export function useBlocker( blocker, when = true ) 
    const  navigator  = useContext( NavigationContext );

    useEffect( () => 
        if ( ! when ) return;

        const unblock = navigator.block( ( tx ) => 
            const autoUnblockingTx = 
                ...tx,
                retry() 
                    // Automatically unblock the transition so it can play all the way
                    // through before retrying it. TODO: Figure out how to re-enable
                    // this block if the transition is cancelled for some reason.
                    unblock();
                    tx.retry();
                ,
            ;

            blocker( autoUnblockingTx );
         );

        return unblock;
    , [ navigator, blocker, when ] );

/**
 * Prompts the user with an Alert before they leave the current screen.
 *
 * @param  message
 * @param  when
 */
export function usePrompt( message, when = true ) 
    const blocker = useCallback(
        ( tx ) => 
            // eslint-disable-next-line no-alert
            if ( window.confirm( message ) ) tx.retry();
        ,
        [ message ]
    );

    useBlocker( blocker, when );

那么用法是:

const MyComponent = () => 
    const formIsDirty = true; // Condition to trigger the prompt.
    usePrompt( 'Leave screen?', formIsDirty );
    return (
        <div>Hello world</div> 
    );
;

【讨论】:

【参考方案3】:

为想要自定义 UI pop-up/modal box 而不是浏览器的 default prompt 并且他们正在使用 react-router (v4)history 的人发布此内容。

您可以使用custom history 并配置您的router 喜欢

import createBrowserHistory from 'history/createBrowserHistory'
export const history = createBrowserHistory()

... 
import  history  from 'path/to/history';

<Router history=history>
  <App/>
</Router>

然后在你的自定义提示组件中你可以使用history.blocklike

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 添加到您想要阻止导航的组件中。

【讨论】:

【参考方案4】:

@Devb 你的问题和更新非常有帮助,为我节省了很多时间。谢谢!根据您的代码创建了一个 HOC。可能对某人有用。 包装组件上的道具:

setPreventNavigation - 设置何时阻止导航

provideLeaveHandler - 设置当您尝试更改路线并且导航被阻止时将运行的函数

confirmNavigation - 继续导航

cancelNavigation - 停止导航

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

【讨论】:

请注意,您在 handleBlockedNavigation 回调中拥有的布尔返回值无效 - 我最初在我的答案中也有这个,也是基于 @Devb 的代码和框,但在查看了打字稿之后规范意识到任何返回值都会被忽略,因此最好将它们删除以避免任何解释它们具有故意的效果。如果您确实想有条件地取消阻止,则需要使用nextLocation.retry()【参考方案5】:

您似乎正在寻找beforeunload event。

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

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

希望这会有所帮助。

【讨论】:

【参考方案6】:

他的answer 中提到的钩子@gdh 已被react-router 的开发团队删除。因此,您不能在当前版本的 react-router (v6) 中使用 usePromptuseBlocker

但团队提到他们正在大力开发这些功能。 reference

如果有人想要实现混音团队为提供挂钩功能所做的更改,您可以查看 github 上的这个答案。 here

【讨论】:

【参考方案7】:

当当前路由的表单有未保存的修改时,我尝试使用与 react router v6 beta 的 useBlocker 挂钩集成的自定义“令人愉快的” UI 确认对话框来阻止路由转换。我从该问题底部UPDATED 部分中链接的codesandbox 中的代码开始。我发现这个自定义钩子实现不能满足我的所有需求,所以我对其进行了调整以支持可选的正则表达式参数来定义一组不应被阻止的路由。另外值得注意的是,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 Router v6 中,如何在离开页面/路由之前检查表单是不是脏的主要内容,如果未能解决你的问题,请参考以下文章

React Router v6 中的前导可选路径参数

react之react-router v6

在 React Router v6.+ 中渲染多个元素

如何使此代码与 react-router v6 兼容

如何从 react-router-dom v6 中的其他位置/功能获取路由?

React-Router-Dom v6 保护路由