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


    前言

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

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

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

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

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

    正文

    场景一:使用 创建的路由

    使用 创建的路由多出现于 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这个实现看似没有问题,但是存在一个限制,由于我们还是使用了 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}
        />
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

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

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

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

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

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    场景一 需要考虑 history.listen 问题,那么在该场景下需不需要考虑 history.listen ?答案是不需要的,从如上编码的命名可以看出,DataBrowserRouter 内部已经额外实现了一套 subscribe 订阅机制,无需我们再做任何修改。

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

    场景三:脱离 react router 的 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 });
      }
      // ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    从而我们可以将 history 实例完全外置,实现在任意位置使用的目的:

    // history.ts
    
    import { createBrowserHistory } from 'history'
    
    export const history = createBrowserHistory()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    // App.tsx
    
    import { BrowserRouter } from 'react-router-use-history'
    import { history } from './history'
    
    function Root() {
      return (
        <BrowserRouter history={history}>
          {/* ... */}
        </BrowserRouter>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    // anywhere.ts
    
    import { history } from './history'
    
    // ...
    
    history.push('...')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    到此为止,我们实现了 history 的完全自定义外置。

    总结

    从某种程度上来说,使用 UNSAFE_* 的 router 内部 context 确实可以解决我们的问题,但也带来了一定的不确定性。

    此外,react router v6 的 api 变化速度很快,保持敏感持续跟进才是王道。

    本文完整代码见于:react-router-use-history

  • 相关阅读:
    成都理工大学_Python程序设计_第1章
    国内MES系统应用研究报告:“企业MES应用现状”| 百世慧®
    C++20:constexpr、consteval和constinit
    wy的leetcode刷题记录_Day41
    【2】CH347应用--在OpenOCD添加CH347-JTAG接口
    MySQL主从复制与读写分离
    vector迭代器失效问题
    在IDEA 中的配置Tomcat
    神州三层交换机DHCPv6中继服务配置
    零基础学前端(七)将项目发布成网站
  • 原文地址:https://blog.csdn.net/qq_21567385/article/details/126945139