useBlocker 防止页面跳转导致的表单丢失

本文介绍了在ReactRouterv6及其早期版本中,如何使用unstable_useBlocker替代Prompt组件来防止用户离开页面,并展示了如何创建自定义确认对话框。作者还提到了文档混乱和状态管理问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于useBlocker

The hook allows you to prevent the user from navigating away from the
current location, and present them with a custom UI to allow them to
confirm the navigation

在react-router的v6版本之前,我们会使用<Prompt />组件来拦截路由的跳转。但最近新开的项目中,发现了没有<Prompt />组件了,所以在开发的版本中,使用了prompt的替代品----useBlocker

但是在v6的早期版本中,如6.7等版本中,该钩子还是属于unsafe的状态,所以需要用到 unstable_useBlocker 来开发,而不是 useBlocker 。吐槽一句,现在的 react-router 让人很懵逼,每次在找文档的过程中,总是看到一大堆的状态管理,整的我以为自己在看react-redux了。。。

useBlocker的使用

吐槽完了react-router的文档之后,是时候进入正题了。

下面会用一个简单的例子展示下如何使用useBlocker~

老规矩,当前的示例中,react-router的版本如下:

"react-router-dom": "^6.16.0",

因为是项目所使用的版本我也不会去做出更改,所以下面的示例依旧使用的是 unstable_useBlocker

首先,我们会定义一个组件,由于起到的作用类似之前的Prompt,所以我将之命名为usePrompt。

import { useEffect, useRef } from 'react';
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
import { BlockerFunction } from '@remix-run/router/router';
​
export function usePrompt(onLocationChange: BlockerFunction, hasUnsavedChanges: boolean) {
  const blocker = useBlocker(hasUnsavedChanges ? onLocationChange : false);
  const prevState = useRef(blocker.state);useEffect(() => {
    if (blocker.state === 'blocked') {
      blocker.reset();
    }
    prevState.current = blocker.state;
  }, [blocker]);
}


在上述的代码中,useBlocker 钩子接受布尔值或阻止函数作为其参数,类似于 Prompt 组件中的 message 属性。

该函数的一个参数是下一个位置,我们使用它来确定用户是否正在离开我们的表单。

如果是这种情况,我们利用浏览器的 window.confirm 方法显示一个对话框,询问用户确认重定向或取消它。最后,我们在 usePrompt 钩子中抽象出阻止逻辑并管理阻止器的状态。

将其封装为一个钩子后…吐槽一句…我现在也不得不用钩子了…
在我们的页面中,我们就可以使用其来做跳转的判定了。

​useBlocker 的图像结果

从组件的封装角度来说,由于这一块主要是做表单提交的校验,所以下面会创建一个名字叫 FormPrompt 的组件。代码如下:

import React, { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDisclosure } from '@nextui-org/react';
import { BlockerFunction } from '@remix-run/router/router';
import { Location } from '@remix-run/router/dist/history';
import CancelPlanCreationModal from 'pages/Plans/CancelPlanCreationModal';
import { usePrompt } from 'components/FormPrompt/usePrompt';
​
interface Props {
  /**
   * The `hasUnsavedChanges` prop indicate whether the form has unsaved changes.
   */
  hasUnsavedChanges: boolean;
}
​
const FormPrompt: React.FC<Props> = ({ hasUnsavedChanges }) => {
  const navigate = useNavigate();
  const {
    isOpen: isLeaveConfirmModalOpen,
    onOpen: handleOpenLeaveConfirmModal,
    onOpenChange: handleLeaveConfirmModalOpenChange
  } = useDisclosure();
  const [lastLocation, setLastLocation] = useState<Location | null>(null);
  const [confirmedNavigation, setConfirmedNavigation] = useState(false);
​
  const handleConfirmNavigation = useCallback(() => {
    // confirm navigation after 350ms to allow the modal to finish closing animation before redirecting
    // in order to fix the issue: Failed to execute 'createTreeWalker' on 'Document': parameter 1 is not of type 'Node'
    setTimeout(() => {
      setConfirmedNavigation(true);
    }, 350);
  }, []);
​
  const handleCancelNavigation = useCallback(() => {
    setLastLocation(null);
  }, []);
​
  const onLocationChange: BlockerFunction = useCallback(
    ({ nextLocation, currentLocation }) => {
      if (!confirmedNavigation && nextLocation.pathname !== currentLocation.pathname && hasUnsavedChanges) {
        setLastLocation(nextLocation);
        handleOpenLeaveConfirmModal();
        return true;
      }
      return false;
    },
    [confirmedNavigation, hasUnsavedChanges, handleOpenLeaveConfirmModal]
  );usePrompt(onLocationChange, hasUnsavedChanges);useEffect(() => {
    if (confirmedNavigation && lastLocation) {
      navigate(lastLocation.pathname);
      setConfirmedNavigation(false);
    }
  }, [confirmedNavigation, lastLocation, navigate]);return (
    <CancelPlanCreationModal
      isOpen={isLeaveConfirmModalOpen}
      onOpenChange={handleLeaveConfirmModalOpenChange}
      onLeave={handleConfirmNavigation}
      onCancel={handleCancelNavigation}
      title="Leave plan creation?"
    />
  );
};
​
export default FormPrompt;


先要明确的是,该组件的作用是做路由跳转的判断,类似Vue的离开路由,所以我们需要用到useNavigate作为跳转的操作,而页面是否需要提示,则我们需要自己传入到这个组件中。

理解了这些基本东西之后,首先我们可以看到我们会判断传进来的hasUnsavedChanges参数,当其改变的时候,我们需要去判断是否打开弹窗。

  const onLocationChange: BlockerFunction = useCallback(
    ({ nextLocation, currentLocation }) => {
      if (!confirmedNavigation && nextLocation.pathname !== currentLocation.pathname && hasUnsavedChanges) {
        setLastLocation(nextLocation);
        handleOpenLeaveConfirmModal();
        return true;
      }
      return false;
    },
    [confirmedNavigation, hasUnsavedChanges, handleOpenLeaveConfirmModal]
  );
简化版的代码如下

 const onLocationChange = useCallback(
    ({ nextLocation }) => {
      if (!stepLinks.includes(nextLocation.pathname) && hasUnsavedChanges) {
        return !window.confirm(
          "You have unsaved changes, are you sure you want to leave?"
        );
      }
      return false;
    },
    [hasUnsavedChanges]
  );

接着,我们将其传入到我们定义好的hook中

  usePrompt(onLocationChange, hasUnsavedChanges);

最后再将想要显示的组件显示出来

  return (
    <CancelPlanCreationModal
      isOpen={isLeaveConfirmModalOpen}
      onOpenChange={handleLeaveConfirmModalOpenChange}
      onLeave={handleConfirmNavigation}
      onCancel={handleCancelNavigation}
      title="Leave plan creation?"
    />

基本流程如此,为了简单点,下面会贴上简单版的代码方便参考

function Home() {
  const [value, setValue] = useState("");
  const blocker = useBlocker(!!value);
  
  useEffect(() => {
    if (blocker.state === "blocked") {
      Modal.confirm({
        message: "确认离开吗",
        onOk: () => {
          blocker.proceed?.();
        },
        onCancel: () => {
          blocker.reset?.();
        },
      });
    }
  }, [blocker]);return (
    <div>
      <Link to="/about" />
      <input value={value} onChange={(e) => setValue(e.target.value)} />
    </div>
  );
}

在上述代码中,直接简单的判断输入框来决定是否跳转,只是项目可能没那么简单,要注意封装噢…不然挨骂了别找我…

不足点

毕竟useBlocker也是根据react-router来干活的,所以当你用window.location来跳转的时候,是无法做到监听的​。

在这里插入图片描述
公众号文章链接~求关注

创作不易,感谢观看,希望能帮到您

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值