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