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 的一部分。
本文并不旨在介绍两种依赖库的具体用法(具体用法请参考官方文档和教程),而主要阐述其实现方式和原理,总结具体的实践方式和注意事项。在主要内容之前,首先简要介绍下两个库的功能:
-
React-Router
React-Router 做的最重要的事就是将浏览器 URL 与程序联系起来(借助 history 库),它为 React 提供了声明式的路由系统,通过其提供的导航组件,我们能够方便地使用 URL 来控制状态的变化和组件的切换。
-
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 架构中的情况。本部分的下文将分为两部分:
-
手动管理,也就是不使用 React-Router-Redux;
-
借助 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 已经成为其一部分,但官方还是就其是否应该在项目中使用进行了建议:
-
希望在项目中使用完全使用 store 管理路由数据
-
希望使用 dispatch action 的方式进行导航(修改路由)
-
希望调试时路由支持 time travel
上面是使用 React-Router-Redux 的原则,当然一定程度上也可以是决定将路由纳入 store 管理的原则。我觉得还可以增加两条:
-
项目抽象中,路由信息应该作为一种全局的状态管理
-
有 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 后有多种方式能够修改路由信息,如:
-
history.method
-
context.router.method
-
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 数据进行路由信息传递。