众所周知,react-router
/ react-router-dom
在 v6 版本取消了对 remix-run / history 的依赖,大幅减负,内部自己实现了更简约、轻量的 history
,所以不再提供 useHistory
方法,这会导致:
如果从 react router v5 升级,迁移困难。
history.listen
无法使用,新的 useLocation
、useNavigate
学习成本等。
为了解决这个问题,本文介绍 三种不同场景 下重新实现 useHistory
的思路。
创建的路由使用
创建的路由多出现于 react router v6.4
以前,这是 < v6.4
的路由推荐创建方式,不带有 remix router 数据流功能。同时,在 >= v6.4
以后,react router v6 不再推荐使用该方式创建路由,但仍可继续使用。
实例:
import { BrowserRouter, Routes, Route } from 'react-router-dom'
function Root() {
return (
<BrowserRouter>
<Routes>
<Route path='/' elements={<Page />} />
</Routes>
</BrowserRouter>
)
}
这里基于以上层级结构,分析源码后我们直接给出两个结论:
react router 导出了 UNSAFE_NavigationContext
等内部 context 可以 hack 式的获取到 router 内部的数据。
在 UNSAFE_NavigationContext
层上,react router v6 提供了 navigator
,他即是内部创建出来的 history
。
有了以上两个结论,我们可以编写 useHistory
的实现:
// useHistory.ts
import { UNSAFE_NavigationContext } from 'react-router-dom'
import { type History } from '@remix-run/router'
export const useHistory = () => {
const navigator = useContext(UNSAFE_NavigationContext).navigator
return navigator as History
}
这个实现看似没有问题,但是存在一个限制,由于我们还是使用了 react router v6 内部创建的轻量化 history
,但是这个 history
限制了 history.listen
的监听器数量 最多添加 1 个 ,不巧的是在
中已经使用了 1 次 history.listen
,这意味着我们再无法使用 history.listen
了。
history.listen
的解法如果你并不使用 history.listen
,则可以安心使用这个 useHistory
的实现。但如果要使用,则需要重新实现
( 源码见:function BrowserRouter() )
// 源码部分
export function BrowserRouter({
basename,
children,
window,
}: BrowserRouterProps) {
let historyRef = React.useRef<BrowserHistory>();
if (historyRef.current == null) {
historyRef.current = createBrowserHistory({ window, v5Compat: true });
}
let history = historyRef.current;
let [state, setState] = React.useState({
action: history.action,
location: history.location,
});
// 🔴 这里已经使用了仅有 1 次的 `history.listen`
React.useLayoutEffect(() => history.listen(setState), [history]);
return (
<Router
basename={basename}
children={children}
location={state.location}
navigationType={state.action}
navigator={history}
/>
);
}
如上源码所示,我们在重新实现
时,新增一层订阅机制即可,让 history.listen()
触发时,不光执行 setState
还执行我们的订阅机制。
此处实现代码较多,展示略过,详见 react-router-use-history > BrowserRouter 。
到此为止,我们可以完美模拟
创建的路由中
useHistory
的实现。
createBrowserRouter
和
创建的路由createBrowserRouter
+
创建路由法是 react router >= v6.4
后带来的,在 react router v6.4-pre
预发阶段,他被称为 DataBrowserRouter
,意味着使用 createBrowserRouter()
创建的路由具有 remix router 数据流的功能(如 loader
/ action
)。
同时,createBrowserRouter
在 >= v6.4
以后也成为 react router v6 文档中推荐的创建路由方式。在该场景下,即使你不使用 remix router 带来的数据流功能,在其他方面都可以获得与
路由近似一致的体验。
useHistory
的实现与
不同的是,具备 remix router 数据流功能的 DataBrowserRouter
内部实现更为复杂,我们的思路与 场景一 相同,从 context 中 hack 数据:
// useHistory.ts
import { UNSAFE_DataRouterContext } from 'react-router-dom'
import { type History } from '@remix-run/router'
export const useHistory = () => {
const context = useContext(UNSAFE_DataRouterContext)
const navigator = context?.navigator
const state = context?.router?.state
const subscribe = context?.router?.subscribe
return {
get location() {
return state?.location
},
get action() {
return state?.historyAction
},
listen: subscribe,
...navigator,
} as History
}
场景一 需要考虑 history.listen
问题,那么在该场景下需不需要考虑 history.listen
?答案是不需要的,从如上编码的命名可以看出,DataBrowserRouter
内部已经额外实现了一套 subscribe 订阅机制,无需我们再做任何修改。
到此为止,我们可以完美模拟
createBrowserRouter
创建的路由中useHistory
的实现。
history
此种场景是 场景一 的延伸,在该场景下:
history
,这意味着 history
可以在任意位置使用,不再局限于 react router 。在 场景一 中我们 fork 了
的实现,history
的创建时机也恰好在
中 ,从而我们可以从外部提供完全自定义的 history
实例:
import { type BrowserRouterProps } from 'react-router-dom'
import { type History as LegacyHistory } from 'history'
import { type BrowserHistory } from '@remix-run/router'
interface IBrowserRouterProps extends BrowserRouterProps {
history?: BrowserHistory | LegacyHistory
}
export function BrowserRouter({
basename,
children,
window,
history: specifiedHistory
}: IBrowserRouterProps) {
let historyRef = React.useRef<BrowserHistory>();
if (historyRef.current == null) {
// 🟢 ↓ 在此处,我们优先使用传入的 history 实例
historyRef.current = specifiedHistory || createBrowserHistory({ window, v5Compat: true });
}
// ...
从而我们可以将 history
实例完全外置,实现在任意位置使用的目的:
// history.ts
import { createBrowserHistory } from 'history'
export const history = createBrowserHistory()
// App.tsx
import { BrowserRouter } from 'react-router-use-history'
import { history } from './history'
function Root() {
return (
<BrowserRouter history={history}>
{/* ... */}
</BrowserRouter>
)
}
// anywhere.ts
import { history } from './history'
// ...
history.push('...')
到此为止,我们实现了
history
的完全自定义外置。
从某种程度上来说,使用 UNSAFE_*
的 router 内部 context 确实可以解决我们的问题,但也带来了一定的不确定性。
此外,react router v6 的 api 变化速度很快,保持敏感持续跟进才是王道。
本文完整代码见于:react-router-use-history