关于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 (<CancelPlanCreationModalisOpen={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 (<CancelPlanCreationModalisOpen={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来跳转的时候,是无法做到监听的。
公众号文章链接~求关注
创作不易,感谢观看,希望能帮到您