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