React-router V6 拦截 路由跳转
目标实现效果:拦截路由变化做自定义处理,比如在一个表单尚未填写完成,用户就要离开当前页面此时需要给用户做一个提醒,如下图所示
![](http://mdimg.wxwenku.com/getimg/6b990ce30fa9193e296dd37902816f4b51ddeb1da19ea3360c7b1f586c819e796b6c9daa18b27d5016ad908e189e76f5.jpg)
先说一下背景知识:React-router 是由三个库一起组成的 history、react-router、react-router-dom
我们平时需要用到的是 react-router-dom
v5 版本实现路由拦截
-
以前在使用 v5 版本时,是这样实现路由拦截的
// 文档:http://v5.reactrouter.com/core/api/Prompt <Prompt when={boolean} // 组件何时激活 message={(location, action) => { // 做一些拦截操作 location 要前往的路由,此时可以先保存下来后续使用 // return false 取消跳转 比如此时弹起一个自定义弹窗, // return true 允许跳转 }} />
v6 版本实现
- v6 版本没有了
Prompt
组件,Google 搜索之后找到了这个stackoverflow v6 beta 时提供了两个 hooksuseBlocker
/usePrompt
可以用来实现路由拦截,但是到正式版的时候这两个 hook 就被移除了,这个issue 里面有讨论,这里有人找出了解决方案就是把删除的这两个 hooks 再加回去 :joy: - 其实路由拦截功能主要是用到了
history
库里面的block
方法,这里是相关代码 -
history.block will call your callback for all in-page navigation attempts, but for navigation that reloads the page (e.g. the refresh button or a link that doesn't use history.push) it registers a beforeunload handler to prevent the navigation. In modern browsers you are not able to customize this dialog. Instead, you'll see something like this (Chrome):
- 简单的翻译下就是
histoy.block
会阻止页面中的所有导航并调用callback,但是直接关闭 tab 页或是刷新会注册beforeunload
事件继而触发浏览器的默认询问弹窗,不支持去除默认弹框,我下面采用了一种 hack 的办法来去除 默认询问弹框
-
完整代码
import { History, Transition } from 'history' import { useContext, useEffect } from 'react' import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom' type ExtendNavigator = Navigator & Pick<History, 'block'> export function useBlocker(blocker: (tx: Transition) => void, when = true) { const { navigator } = useContext(NavigationContext) useEffect(() => { if (!when) return // 如不需要刷新页面或关闭tab时取消浏览器询问弹窗,下面的绑定事件则不需要 window.addEventListener('beforeunload', removeBeforeUnload) const unblock = (navigator as any as ExtendNavigator).block(tx => { const autoUnblockingTx = { ...tx, retry() { unblock() tx.retry() }, } blocker(autoUnblockingTx) }) // 由于无法直接 remove history 库中绑定的 beforeunload 事件,只能自己在绑定一个 beforeunload 事件(在原事件之前),触发时调用 unblock // function removeBeforeUnload() { unblock() } return () => { unblock() window.removeEventListener('beforeunload', removeBeforeUnload) } }, [when]) }
-
使用
useBlocker
export default function UnsavedPrompt({ when }: Iprops): JSX.Element { const [open, setOpen] = useState(false) const blockRef = useRef<any>(null) useBlocker(tx => { setOpen(true) blockRef.current = tx }, when) return ( <Modal open={open} toggle={() => setOpen(false)} onCancel={() => blockRef.current?.retry()} onOk={() => setOpen(false)} > <p className='text-center text-light-700 text-sm'> You have unsaved change, exit without saving? </p> </Modal> ) }
注意
- 书写本文的时间是 2022-08-11 ,
react-router/react-router-dom
的最新版本为6.3.0
,后续可能随着 react-router-dom 的升级可能还会加回来该功能,上述代码仅供参考
分割线
上面关于 React-router v6 路由拦截的写法就已经分享完了,下面再顺道记录一下如何比较表单变化即触发路由拦截的条件,主要实现了一个 useCompare
的 hook 来做的
- 分析:对比表单前后两次数据是否发生变化,无外乎就是把表单的初始数据存一分,然后与正在操作的表单进行深对比,但是由于表单会存在 input 这种组件,他的变化频率比较快所以要做防抖处理,下面是 useCompare 的具体代码
-
先来看一下 useCompare 如何使用
import { useCompare } from 'hooks/useDebounce' const compareFunc = useCompare() useEffect(() => { compareFunc(formData, formDataInit, (flag: boolean) => setFormIsDirty(flag)) }, [formData, formDataInit]) // 这里的 formDataInit 一般要在初始状态时从 formData 深拷贝一份出来
-
useCompare 实现
type Tcb = (args: boolean) => void // debounce/compare hooks export function useCompare() { const compareFunc = useDebounce(compare) return compareFunc } function compare(a: any, b: any, fn: Tcb) { fn(!isEqual(a, b)) }
-
因为要做防抖处理,首先要实现一个 useDebounce,这里选择自己写了,没有用现成的
import { useRef } from 'react' type TdebounceFnType = (...args: any[]) => void export default function useDebounce(fn: TdebounceFnType, wait = 1000) { const debounceFnRef = useRef(debounce(fn, wait)) return debounceFnRef.current } // debounce 原始函数 export function debounce(this: any, fn: TdebounceFnType, wait = 1000) { let timer: NodeJS.Timeout | null = null const ctx = this return function (...args: any[]) { timer && clearTimeout(timer) timer = setTimeout(() => { fn.apply(ctx, args) }, wait) } }
「其他文章」
- 性能调优——小小的log大大的坑
- 用Vue.js写一个命令行贪吃蛇游戏
- 1582. 二进制矩阵中的特殊位置 : 简单模拟题
- elementui源码学习之仿写一个el-switch
- 646. 最长数对链 : 常规贪心 DP 运用题
- 记一个“奇葩”需求的实现
- 音视频开发进阶|第六讲:色彩和色彩空间·上篇
- 在线数据迁移,数字化时代的必修课——京东云数据迁移实践
- 刘亦菲生日当天,引发了我对正则的思考
- 还在手写SQL实现?试试MyBatis-Plus同款IDEA插件吧!提示太全了,还能一键生成代码!
- 聊聊如何利用管道模式来进行业务编排(上篇)
- elementui源码学习之仿写一个el-collapse
- 云音乐iOS端网络图片下载优化实践
- 为什么基于树的模型在表格数据上仍然优于深度学习
- 793. 阶乘函数后 K 个零 : 经典「数学 二分」运用题
- Notion 程序猿必备笔记软件
- 662. 二叉树最大宽度 : 简单 DFS 运用题
- jsonp 原理详解及 jsonp-pro 源码解析
- 教程 - 深度探讨在 Vue3 中引入 CesiumJS 的最佳方式
- elementui源码学习之仿写一个el-link