2022 第7週 實作筆記:在離開路由前顯示確認視窗
總結
本篇筆記記錄如何使用 react-router-dom (v5) 的 useHistory()
與 Web API beforeunload
event 在以下時間點跳出確認訊息:
- 使用者點擊重新整理(
beforeunload
) - 使用者關閉分頁(
beforeunload
) - 使用者按下「上(下)一頁」準備離開當下畫面(
history.block()
)
原始碼:https://github.com/tzynwang/react-router-block/tree/version/2
版本
react-router-dom: 5.3.0
webpack: 5.69.0
筆記
useBlock
import { useEffect } from 'react' | |
import { useHistory } from 'react-router-dom' | |
import { UnregisterCallback } from 'history' | |
interface useBlockArgs { | |
block?: boolean | |
} | |
let BLOCK: UnregisterCallback | |
export default function useBlock(args: useBlockArgs) { | |
const history = useHistory() | |
const { block } = args | |
const preventCloseAndRefresh = (event: BeforeUnloadEvent) => { | |
const e = event || window.event | |
e.preventDefault() | |
if (e) e.returnValue = '' | |
return '' | |
} | |
useEffect(() => { | |
if (block === false) return | |
window.addEventListener('beforeunload', preventCloseAndRefresh) | |
BLOCK = history.block(() => { | |
const leave = window.confirm('Leave this page?') | |
if (!leave) { | |
return false | |
} | |
return undefined | |
}) | |
return () => { | |
window.removeEventListener('beforeunload', preventCloseAndRefresh) | |
BLOCK() | |
} | |
}, [block]) | |
} |
-
beforeunload
能偵測的是「重新整理」與「關閉分頁」這兩種行為,而使用者點擊「上(下)一頁」會由useHistory()
產生的history
instance 來阻擋 -
從 args 傳入
block
參數,讓 useBlock 可以在特定條件下解除阻擋行為(例:使用者已經填完表單所有內容) -
第 25-31 行:當引用 useBlock 的元件掛載至畫面上時,將
history.block()
回傳的 instance 保存到變數BLOCK
中,並在引用 useBlock 的元件從畫面上卸載時,執行BLOCK
-
第 26 行:使用
window.confirm
彈出瀏覽器原生的對話框,詢問使用者是否離開網頁,選擇不離開(!leave
)則回傳false
取消離開的行為 -
參考
@types/history
可得知history.block()
與其 args 的 type 分別如下列:export interface History<HistoryLocationState = LocationState> { block( prompt?: boolean | string | TransitionPromptHook<HistoryLocationState> ): UnregisterCallback; } export type TransitionPromptHook<S = LocationState> = ( location: Location<S>, action: Action ) => string | false | void;
為了讓 type check 不會報錯,在使用者確認離開畫面時,讓傳入
history.block()
的匿名函式回傳undefined
而非true
(第 30 行)
無條件阻擋:About 元件
import React, { memo } from 'react' | |
import useBlock from '@Hooks/useBlock' | |
function AboutPage(): React.ReactElement { | |
useBlock({}) | |
return <div>this is the about page.</div> | |
} | |
export default memo(AboutPage) |
直接在元件中呼叫 useBlock()
,並傳入空物件(不需解除條件)
有條件阻擋:Form 元件
import React, { memo, useState, useEffect } from 'react' | |
import useBlock from '@Hooks/useBlock' | |
function FormPage(): React.ReactElement { | |
const [block, setBlock] = useState<boolean>(true) | |
useBlock({ block }) | |
useEffect(() => setBlock(true), []) | |
return ( | |
<div> | |
<label htmlFor="fname">First name:</label> | |
<br /> | |
<input type="text" id="fname" name="fname" value="John" required /> | |
<br /> | |
<label htmlFor="lname">Last name:</label> | |
<br /> | |
<input type="text" id="lname" name="lname" value="Doe" required /> | |
<br /> | |
<br /> | |
<button onClick={() => setBlock(false)}>submit</button> | |
<br /> | |
Block status:{' '} | |
{block | |
? 'page is block, click the submit button to unblock' | |
: 'page is NOT block'} | |
</div> | |
) | |
} | |
export default memo(FormPage) |
傳入 useState
變數 block
,在使用者按下送出表單的按鈕時,將 block
設定為 false
,並在畫面重新整理(元件重新掛載到畫面上後),重設 block
為 true
避免 F5 後阻擋失效
webpack devServer 設定
- 在後端 server 沒有配合的情況下,使用
BrowserRouter
來處理路由會導致畫面重新整理、或是使用者直接輸入路由時遭遇 404 - 如果在開發時使用 webpack devServer 來執行 react APP,則設定
webpack.config.js
的devServer
其historyApiFallback: true
即可使用BrowserRouter
devServer: { historyApiFallback: true; }
- 若 react APP 是部署到 GitHub Pages(或任何無法配合 history mode 的 server)則需使用
react-router-dom
的HashRouter
來處理路由,而路由會帶上井字號