React 路由状态管理总结

一、依赖(Dependencies)

在一般 SPA 开发中,路由的管理十分重要。作为 React 技术体系中的一部分,官方维护的 React-Router 则是首选的路由库。

在应用 Redux 模式后,React-Router 与 Redux 的配合引发了新的问题,是否需要将路由纳入 store 进行管理?如何将路由纳入 store 进行管理?这些都是需要考虑的问题。我们将在后文讨论第一个问题,而为了解决上述第二个问题,React-Router-Redux 这个轻量级的扩展库应运而生并得到广泛应用。

另外需要说明的是,长久以来 React-Router 与 React-Router-Redux 是两个独立的库,但在 React-Router 4.x 版本以后,React-Router-Redux 已经成为了 React-Router 的一部分。

本文并不旨在介绍两种依赖库的具体用法(具体用法请参考官方文档和教程),而主要阐述其实现方式和原理,总结具体的实践方式和注意事项。在主要内容之前,首先简要介绍下两个库的功能:

  1. React-Router

    React-Router 做的最重要的事就是将浏览器 URL 与程序联系起来(借助 history 库),它为 React 提供了声明式的路由系统,通过其提供的导航组件,我们能够方便地使用 URL 来控制状态的变化和组件的切换。

  2. React-Router-Redux

    按照官方的说法,其实现了「deep integration of react-router and redux」,即 React-Router 与 Redux 的深度集成,它将路由完全纳入 store 中进行管理,使 store 成为了 URL(或者说是 history)的数据来源,也使我们能够通过 dispatch action 的方式来修改 URL。我们将在后文介绍它的实现原理。

二、实践

路由状态并非一定要介入 Redux 架构中。在一些简单的应用场景下,只需要使用 React-Router 提供的声明式组件(Router, Route, Link 等)即可方便的实现 URL 导航。在一些稍复杂的场景中,只要保证遵循 React 单向数据流动方式,遵照使用方法,也可以完成进行路由信息的读取和触发变更,其过程如下图所示。(使用方法请参照 React-Router 文档和教程)

image

但在这里,我们主要讨论将路由状态纳入 Redux 架构中的情况。本部分的下文将分为两部分:

  1. 手动管理,也就是不使用 React-Router-Redux;

  2. 借助 React-Router-Redux 管理,这也是讨论的重点。

2.1、手动管理

在不借助其他库,一种简单的做法是手动将路由状态纳入 store 中管理,当 URL 改变时同步修改 store 中的状态。

image

如上图,在手动同步环节,通过一套 Redux 机制,实现了路由信息在 store 中的存储。history 作为数据来源,通过监听 history,当 URL 状态改变时 dispatch 相应 action (例如 type = LOCATION_CHANGE),通过添加的 reducer 将 location 信息同步到 store。通过这种方式,组件就可以获取 store 中的 location 状态信息,这也是目前 react-redux-starter-kit 采用的方式。

这种相对原始的方式有一定弊端:

  • 没有将路由完全纳入 Redux 管理。

  • 路由不支持 time travel。

  • history 实际也是 react-router 的路由数据来源,这就导致我们 store 中存储的 location 数据与 react-router 并不一定同步。(例如,这会导致文末讨论的重复渲染问题)

2.2、使用 React-Router-Redux

下面我们讨论文首提出的问题一:是否需要将路由纳入 store 进行管理。虽然在 react-router 4.x 版本后,react-router-redux 已经成为其一部分,但官方还是就其是否应该在项目中使用进行了建议:

  1. 希望在项目中使用完全使用 store 管理路由数据

  2. 希望使用 dispatch action 的方式进行导航(修改路由)

  3. 希望调试时路由支持 time travel

上面是使用 React-Router-Redux 的原则,当然一定程度上也可以是决定将路由纳入 store 管理的原则。我觉得还可以增加两条:

  1. 项目抽象中,路由信息应该作为一种全局的状态管理

  2. 有 Redux 强迫症

2.2.1、原理

通过一张图的方式来了解一下 React-Router-Redux 的实现原理。

image

上图实际上也是 React-Router-Redux 如何将 URL 与 state 同步的过程,在程序中,主要是通过如下的几个重要的 API 实现的:

  • routerMiddleware 与 routerReducer
    routerMiddleware 与 routerReducer 的共同作用,让我们能够处理两种 action 类型:一种类型为 LOCATION_CHANGE,与手动管理过程中相同,它负责修改 store 存储;另一种类型为 CALL_HISTORY_METHOD,这类 action 一般会在组件内派发,它不负责 state 的修改,通过 routerMiddleware 后,会被转去调用 history 方法(如 push, replace 等),以修改 URL 状态。

  • **syncHistoryWithStore ** 顾名思义,这个方法就是处理路由与 store 中信息同步的重要方法。通过这个方法,我们能获得一个新的、增强版的 history 对象,这个对象重写了 history的listen方法,原有的 history.listen只负责 action (LOCATION_CHANGE) 的派发,新的 history.listen则只监听 store 的变化(使用了 store.subscribe),所以当我们在程序内调用 history.listen时,实际上是在监听 store 中的路由信息。

2.2.2、实践:location as a prop

在实际项目应用中,一种较为合理实践方式如下。

image

即将 location 或子属性(如 location.pathname 等)作为属性信息逐层传递,传递给关注路由信息的子组件,这类似于 react-router 原有的使用方法,区别是,在改变 URL 时,使用了 dispatch action 的方式。

三、建议

3.1、 谨慎地使用 state.routing

一般地,在使用 React-Router-Redux 时,路由信息在 store 中会以 routing.locationBeforeTransition 的形式体现。我们在上文的实践中并没有直接从 store 中获取这个状态,实际上官方也不建议这样做,从名字来看,作者已经明确提醒了我们这是一个变化中的值。

You should not read the location state directly from the Redux store. This is because React Router operates asynchronously (to handle things such as dynamically-loaded components) and your component tree may not yet be updated in sync with your Redux state. You should rely on the props passed by React Router, as they are only updated after it has processed all asynchronous code.

不应该直接从 Redux store 中读取路由状态。这是因为 React-Router 的行为是异步的(例如为了处理组件动态加载等),所以你的组件树可能不能跟上 Redux 状态的变化。应该去依赖 React Router 传递的属性,这保证了这些值是在所有异步操作完成后才更新的。

当 routing 中的值已经改变时,React-Router 可能还没有将组件树进行更新完毕,如果使用这个值可能引发一些问题。所以作者依然建议我们采用传递 location 属性的方式读取路由信息,以确保 React-Router 已经处理完毕。

3.2、只传递必要的路由信息

只将必要信息作为 prop 传递,例如 location.pathname、 location.query.page,而不是传递整个 location。这能够尽量避免可能的重复渲染。

3.3、 只使用 dispatch action 的方式修改路由

实际上,除了使用 Link 组件,使用 React-Router-Redux 后有多种方式能够修改路由信息,如:

  1. history.method

  2. context.router.method

  3. dispatch ROUTER-ACTION

笔者仍然建议只使用 dispatch action 方式修改路由,这种方式更为遵循 Redux 流程,同时方便组件的解耦。在实际应用中,应该使用统一的 Action Creator 来创建修改路由的 action。

3.4、 谨慎地使用 withRouter高阶组件(装饰器)

React-Router 提供了withRouter高阶组件以便组件访问路由状态信息(match, location, history),但同时一旦引用的路由属性发生变化就会触发重渲染流程,如果使用不当,则可能导致组件进行多余的重复渲染。

四、常见问题

4.1、re-render(重复渲染)问题

在使用 React-Router 和路由组件异步加载后,一个常见的问题是组件切换时发生意外的重复渲染。 一般情况下(未进行代码分割时),React-Router 在切换路由组件时,过程是这样的:

image

在进行了代码分割后,路由组件改为异步加载,过程变成了这样:

image

由于组件 A 将 location 或其相关属性最为属性 props 传入,location 的变化导致了 props 的改变,此时由于组件 B 还未加载成功,导致组件 A 在卸载前进行没有必要的重渲染。

这个问题一般是因为错误地使用了变化的路由信息,如上文中的 state.routing 信息,由于 state.routing 与 React-Router 路由信息不同步造成的。解决办法:参照上文提出的实践,使用 Route 组件注入的 location 数据进行路由信息传递。

五、参考

  1. https://github.com/reactjs/react-router-redux

  2. https://github.com/reactjs/react-router-tutorial

  3. https://github.com/ReactTraining/history

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容