react router v6实现useHistory与自定义history设计思路

针对 React Router v6 的重大变更,本文介绍了三种不同场景下重新实现 useHistory 的方法,包括使用 BrowserRouter、createBrowserRouter 和完全自定义 history 的情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

众所周知,react-router / react-router-dom 在 v6 版本取消了对 remix-run / history 的依赖,大幅减负,内部自己实现了更简约、轻量的 history ,所以不再提供 useHistory 方法,这会导致:

  1. 如果从 react router v5 升级,迁移困难。

  2. history.listen 无法使用,新的 useLocationuseNavigate 学习成本等。

为了解决这个问题,本文介绍 三种不同场景 下重新实现 useHistory 的思路。

  • 注:以下我们均指代的是 react router >= 6 的版本。

正文

场景一:使用 <BrowserRouter> 创建的路由

使用 <BrowserRouter> 创建的路由多出现于 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>
  )
}

这里基于以上层级结构,分析源码后我们直接给出两个结论:

  1. react router 导出了 UNSAFE_NavigationContext 等内部 context 可以 hack 式的获取到 router 内部的数据。

  2. 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 个 ,不巧的是在 <BrowserRouter> 中已经使用了 1 次 history.listen ,这意味着我们再无法使用 history.listen 了。

history.listen 的解法

如果你并不使用 history.listen ,则可以安心使用这个 useHistory 的实现。但如果要使用,则需要重新实现 <BrowserRouter> ( 源码见:function BrowserRouter()

// <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}
    />
  );
}

如上源码所示,我们在重新实现 <BrowserRouter> 时,新增一层订阅机制即可,让 history.listen() 触发时,不光执行 setState 还执行我们的订阅机制。

此处实现代码较多,展示略过,详见 react-router-use-history > BrowserRouter

到此为止,我们可以完美模拟 <BrowserRouter> 创建的路由中 useHistory 的实现。

场景二:使用 createBrowserRouter<RouterProvider> 创建的路由
DataBrowserRouter 的由来

createBrowserRouter + <RouterProvider> 创建路由法是 react router >= v6.4 后带来的,在 react router v6.4-pre 预发阶段,他被称为 DataBrowserRouter ,意味着使用 createBrowserRouter() 创建的路由具有 remix router 数据流的功能(如 loader / action )。

同时,createBrowserRouter>= v6.4 以后也成为 react router v6 文档中推荐的创建路由方式。在该场景下,即使你不使用 remix router 带来的数据流功能,在其他方面都可以获得与 <BrowserRouter> 路由近似一致的体验。

useHistory 的实现

<BrowserRouter> 不同的是,具备 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 的实现。

场景三:脱离 react router 的 history

此种场景是 场景一 的延伸,在该场景下:

  • 我们将完全自定义 history ,这意味着 history 可以在任意位置使用,不再局限于 react router 。

在 场景一 中我们 fork 了 <BrowserRouter> 的实现,history 的创建时机也恰好在 <BrowserRouter> 中 ,从而我们可以从外部提供完全自定义的 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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值