乐趣区

应用connectedreactrouter和reduxthunk打通react路由孤立

redux

在我们开发过程中,很多时候,我们需要让组件共享某些数据,虽然可以通过组件传递数据实现数据共享,但是如果组件之间不是父子关系的话,数据传递是非常麻烦的,而且容易让代码的可读性降低,这时候我们就需要一个 state(状态)管理工具。常见的状态管理工具有 redux,mobx,这里选择 redux 进行状态管理。值得注意的是 React 16.3 带来了全新的 Context API,我们也可以使用新的 Context API 做状态管理。Redux 是负责组织 state 的工具,但你也要考虑它是否适合你的情况。

在下面的场景中,引入 Redux 是比较明智的:

  • 你有着相当大量的、随时间变化的数据
  • 你的 state 需要有一个单一可靠数据来源
  • 你觉得把所有 state 放在最顶层组件中已经无法满足需要了

的确,这些场景很主观笼统。因为对于何时应该引入 Redux 这个问题,对于每个使用者和每个应用来说都是不同的。

对于 Redux 应该如何、何时使用的更多建议,请看:

  • “您可能不需要 Redux”
  • Redux 之道,第 1 部分 - 实现和意图
  • Redux 之道,第 2 部分 - 实践与哲学
  • Redux 常见问题

Redux 的创造者 Dan Abramov 又补充了一句

“ 只有遇到 React 实在解决不了的问题,你才需要 Redux。”

react-redux

react-redux 提供 Provider 组件通过 context 的方式向应用注入 store,然后组件使用 connect 高阶方法获取并监听 store,然后根据 store state 和组件自身的 props 计算得到新的 props,注入该组件,并且可以通过监听 store,比较计算出的新 props 判断是否需要更新组件。

render(<Provider store={store}>
    <ConnectedRouter history={history}>
      <App />
    </ConnectedRouter>
  </Provider>,
  document.getElementById('app')
)复制代码

整合 redux 到 react 应用

合并 reducer

在一个 react 应用中只有一个 store,组件通过调用 action 函数,传递数据到 reducer,reducer 根据数据更改对应的 state。但是随着应用复杂度的提升,reducer 也会变得越来越大,此时可以考虑将 reducer 拆分成多个单独的函数,拆分后的每个函数负责独立管理 state 的一部分。

redux 提供 combineReducers 辅助函数,将分散的 reducer 合并成一个最终的 reducer 函数,然后在 createStore 的时候使用。

整合 middleware

有时候我们需要多个 middleware 组合在一起形成 middleware 链来增强 store.dispatch,在创建 store 时候,我们需要将 middleware 链整合到 store 中,官方提供applyMiddleware(...middleware) 将 middleware 链在一起。

整合 store enhancer

store enhancer 用于增强 store,如果我们有多个 store enhancer 时需要将多个 store enhancer 整合,这时候就会用到compose(...functions)

使用 compose 合并多个函数,每个函数都接受一个参数,它的返回值将作为一个参数提供给它左边的函数以此类推,最右边的函数可以接受多个参数。compose(funA,funB,funC)可以理解为compose(funA(funB(funC()))),最终返回从右到左接收到的函数合并后的最终函数。

创建 Store

redux 通过 createStore 创建一个 Redux store 来以存放应用中所有的 statecreateStore的参数形式如下:

createStore(reducer, [preloadedState], enhancer)复制代码

所以我们创建 store 的代码如下:

import thunk from 'redux-thunk'
import {createStore, applyMiddleware} from 'redux'

import reducers from '../reducers'

const initialState = {}

const store = createStore(reducers, initialState, applyMiddleware(thunk))

export default store 复制代码

之后将创建的 store 通过 Provider 组件注入 react 应用即可将 redux 与 react 应用整合在一起。

注:应用中应有且仅有一个 store

redux 与 react-router

React Router 与 Redux 一起使用时大部分情况下都是正常的,但是偶尔会出现路由更新但是子路由或活动导航链接没有更新。这个情况发生在:

  1. 组件通过 connect()(Comp) 连接 redux。
  2. 组件不是一个“路由组件”,即组件并没有像 <Route component={SomeConnectedThing} /> 这样渲染。

这个问题的原因是 Redux 实现了shouldComponentUpdate,当路由变化时,该组件并没有接收到 props 更新。

解决这个问题的方法很简单,找到 connect 并且将它用 withRouter 包裹:

// before
export default connect(mapStateToProps)(Something)
// after
import {withRouter} from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Something))复制代码

注意 ! ! :

需要注意:withRouter 只是用来处理数据更新问题的。在使用一些 redux 的 connect() 或者 mobx 的 inject() 的组件中,如果依赖于路由的更新要重新渲染,会出现路由更新了但是组件没有重新渲染的情况。这是因为 redux 和 mobx 的这些连接方法会修改组件的shouldComponentUpdate

所以在使用 withRouter 解决更新问题的时候,一定要保证 withRouter 在最外层,比如withRouter(connect()(Component)),而不是 connect()(withRouter(Component))

React Router

将 redux 与 react-router 深度整合

有时候我们可能希望将 redux 与 react router 进行更深度的整合,实现:

  • 将 router 的数据与 store 同步,并且从 store 访问
  • 通过 dispatch actions 导航
  • 在 redux devtools 中支持路由改变的时间旅行调试
集成好处:

1)路由信息可以同步到统一的 store 并可以从中获得

2)可以使用 Redux 的 dispatch action 来导航

3)集成 Redux 可以支持在 Redux devtools 中路由改变的时间履行调试

集成的必要性:

集成后允许 react router 的路由信息可以存到 redux,所以就需要路由组件要能访问到 redux store,这样组件就可以使用 store 的 dispatch action,可以使用 dispatch 带上路由信息作为 action 的负载将路由信息存到 store,同时要能将路由信息从 Redux store 里面同步获取出来

这些可以通过 react-router-reduxconnected-react-routerhistory 两个库将 react-routerredux 进行深度整合实现。

官方文档中提到的是 react-router-redux,并且它已经被整合到了 react-router v4 中,但是根据 react-router-redux 的文档,该仓库不再维护,推荐使用 connected-react-router。

create-react-app 中使用安装所需中间件:

yarn add connected-react-router history redux react-redux redux-devtools-extension react-router-dom 复制代码

然后给 store 添加如下配置:

  • 创建 history 对象,因为我们的应用是浏览器端,所以使用 createBrowserHistory 创建
  • 使用 connectRouter 包裹 root reducer 并且提供我们创建的 history 对象,获得新的 root reducer
  • 使用 routerMiddleware(history) 实现使用 dispatch history actions,这样就可以使用 push('/path/to/somewhere') 去改变路由(这里的 push 是来自 connected-react-router 的)
history.js

import * as createHistory from 'history'
const history = createHistory.createBrowserHistory()

export default history 复制代码
store.js

import thunk from 'redux-thunk'
import {createBrowserHistory} from 'history'
import {createStore, applyMiddleware} from 'redux'
import {connectRouter, routerMiddleware} from 'connected-react-router'
import reducers from '../reducers'
export const history = createBrowserHistory()
const initialState = {}
const store = createStore(connectRouter(history)(reducers),
  initialState,
  applyMiddleware(thunk, routerMiddleware(history))
)
export default store 复制代码

在根组件中,我们添加如下配置:

  • 使用 ConnectedRouter 包裹路由,并且将 store 中创建的 history 对象引入,作为 props 传入应用
  • ConnectedRouter组件要作为 Provider 的子组件
index.js 

import React from 'react'
import {render} from 'react-dom'
import {Provider} from 'react-redux'
import {ConnectedRouter} from 'connected-react-router'
import App from './App'
import store from './redux/store'
import {history} from './redux/store'
render(<Provider store={store}>
    <ConnectedRouter history={history}>
      <App />
    </ConnectedRouter>
  </Provider>,
  document.getElementById('app')
)
复制代码复制代码

这样我们就将 redux 与 react-router 整合完毕。

使用 dispatch 切换路由

完成以上配置后,就可以使用 dispatch 切换路由了:

import {push} from 'react-router-redux'
// Now you can dispatch navigation actions from anywhere!
store.dispatch(push('/about'))复制代码

最终结果如下:

异步任务流管理

实现异步操作的思路

大部分情况下我们的应用中都是同步操作,即 dispatch action 时,state 会被立即更新,但是有些时候我们需要做异步操作。同步操作只要发出一种 Action 即可,但是异步操作需要发出三种 Acion。

  • 操作发起时的 Action
  • 操作成功时的 Action
  • 操作失败时的 Action

为了区分这三种 action,可能在 action 里添加一个专门的 status 字段作为标记位:

{type: 'FETCH_POSTS'}
{type: 'FETCH_POSTS', status: 'error', error: 'Oops'}
{type: 'FETCH_POSTS', status: 'success', response: { ...} }
复制代码复制代码

或者为它们定义不同的 type:

{type: 'FETCH_POSTS_REQUEST'}
{type: 'FETCH_POSTS_FAILURE', error: 'Oops'}
{type: 'FETCH_POSTS_SUCCESS', response: { ...} }
复制代码复制代码

所以想要实现异步操作需要做到:

  • 操作开始时,发出一个 Action,触发 State 更新为“正在操作”,View 重新渲染
  • 操作结束后,再发出一个 Action,触发 State 更新为“操作结束”,View 再次重新渲染

redux-thunk

异步操作至少送出两个 Action,第一个 Action 跟同步操作一样,直接送出即可,那么如何送出第二个 Action 呢?

我们可以在送出第一个 Action 的时候送一个 Action Creator 函数,这样第二个 Action 可以在异步执行完成后自动送出。

componentDidMount() {store.dispatch(fetchPosts())
}
复制代码复制代码

在组件加载成功后,送出一个 Action 用来请求数据,这里的 fetchPosts 就是 Action Creator。fetchPosts 代码如下:

export const SET_DEMO_DATA = createActionSet('SET_DEMO_DATA')
export const fetchPosts = () => async (dispatch, getState) => {store.dispatch({ type: SET_DEMO_DATA.PENDING})
  await axios
    .get('https://jsonplaceholder.typicode.com/users')
    .then(response => store.dispatch({ type: SET_DEMO_DATA.SUCCESS, payload: response}))
    .catch(err => store.dispatch({ type: SET_DEMO_DATA.ERROR, payload: err}))
}
复制代码复制代码

fetchPosts是一个 Action Creator,执行返回一个函数,该函数执行时 dispatch 一个 action,表明马上要进行异步操作;异步执行完成后,根据请求结果的不同,分别 dispatch 不同的 action 将异步操作的结果返回回来。

这里需要说明几点:

  1. fetchPosts返回了一个函数,而普通的 Action Creator 默认返回一个对象。
  2. 返回的函数的参数是 dispatchgetState这两个 Redux 方法,普通的 Action Creator 的参数是 Action 的内容。
  3. 在返回的函数之中,先发出一个 store.dispatch({type: SET_DEMO_DATA.PENDING}),表示异步操作开始。
  4. 异步操作结束之后,再发出一个 store.dispatch({type: SET_DEMO_DATA.SUCCESS, payload: response}),表示操作结束。

但是有一个问题,store.dispatch正常情况下,只能发送对象,而我们要发送函数,为了让 store.dispatch 可以发送函数,我们使用中间件——redux-thunk。

引入 redux-thunk 很简单,只需要在创建 store 的时候使用 applyMiddleware(thunk) 引入即可。

开发调试工具

开发过程中免不了调试,常用的调试工具有很多,例如 redux-devtools-extensionredux-devtoolsstorybook 等。

注意 ,从 2.7 开始,window.devToolsExtension 重命名为window.__REDUX_DEVTOOLS_EXTENSION__/ window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__.

redux-devtools-extension

redux-devtools-extension 是一款调试 redux 的工具,用来监测 action 非常方便。

首先根据浏览器在 Chrome Web Store 或者 Mozilla Add-ons 中下载该插件。

  • store 高级用法 如果 store 使用了中间件 middleware 和增强器enhaners,代码要修改下:
import {createStore, applyMiddleware, compose} from 'redux';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
    reducer, /* preloadedState, */ 
    composeEnhancers(applyMiddleware(...middleware)
  ));
复制代码复制代码
  • 当有特殊扩展选项时,用这么使用:
const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?   
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({// 有指定扩展选项,像 name, actionsBlacklist, actionsCreators, serialize...}) : compose;
const enhancer = composeEnhancers(applyMiddleware(...middleware),
    // 其他 store 增强器(如果有的话));
const store = createStore(reducer, enhancer);
复制代码复制代码
  • 使用 redux-devtools-extension 包 为了简化操作需要安装个 npm 包 npm install --save-dev redux-devtools-extension 使用
import {createStore, applyMiddleware} from 'redux';
import {composeWithDevTools} from 'redux-devtools-extension';
const store = createStore(reducer, 
    composeWithDevTools(applyMiddleware(...middleware),
        // 其他 store 增强器(如果有的话)));
复制代码复制代码
  • 指定扩展名选项:
import {createStore, applyMiddleware} from 'redux';
import {composeWithDevTools} from 'redux-devtools-extension';
const composeEnhancers = composeWithDevTools({// 如果需要,在这里指定名称,actionsBlacklist,actionsCreators 和其他选项});
const store = createStore(reducer, /* preloadedState, */ composeEnhancers(applyMiddleware(...middleware),
  // 其他 store 增强器(如果有的话)));
复制代码复制代码
  • 如果你没有包含其它增强器和中间件的话,只需要使用 devToolsEnhancer
import {createStore} from 'redux';
import {devToolsEnhancer} from 'redux-devtools-extension';
const store = createStore(reducer, /* preloadedState, */ devToolsEnhancer(// 需要的话,在这里指定名称,actionsBlacklist,actionsCreators 和其他选项));
复制代码复制代码
  • 在生产环境中使用 这个扩展在生产环境也是有用的,但一般都是在开发环境中使用它。如果你想限制它的使用,可以用redux-devtools-extension/logOnlyInProduction
import {createStore} from 'redux';
import {devToolsEnhancer} from 'redux-devtools-extension/logOnlyInProduction';
const store = createStore(reducer, /* preloadedState, */         devToolsEnhancer(// actionSanitizer, stateSanitizer 等选项));
复制代码复制代码
  • 使用中间件和增强器时:
import {createStore, applyMiddleware} from 'redux';
import {composeWithDevTools} from 'redux-devtools-extension/logOnlyInProduction';
const composeEnhancers = composeWithDevTools({// actionSanitizer, stateSanitizer 选项});
const store = createStore(reducer, /* preloadedState, */ composeEnhancers(applyMiddleware(...middleware),
  // 其它增强器
));
复制代码复制代码

你将不得不在 webpack 的生产环境打包配置中加上process.env.NODE_ENV': JSON.stringify('production')。如果你用的是create-react-app,那么它已经帮你配置好了

  • 如果你在创建 store 时检查过 process.env.NODE_ENV,那么也包括了生产环境的redux-devtools-extension/logOnly 如果不想在生产环境使用扩展,那就只开启redux-devtools-extension/developmentOnly 就好

点击文章查看更多细节

import thunk from "redux-thunk";
import {createBrowserHistory} from "history";
import {createStore, applyMiddleware} from "redux";
+ import {composeWithDevTools} from "redux-devtools-extension/logOnlyInProduction";
import {connectRouter, routerMiddleware} from "connected-react-router";
import reducers from "../reducers";
export const history = createBrowserHistory();
const initialState = {};
+  const composeEnhancers = composeWithDevTools({
+   // options like actionSanitizer, stateSanitizer
+ });
const store = createStore(connectRouter(history)(reducers),
  initialState,
+  composeEnhancers(applyMiddleware(thunk, routerMiddleware(history)))
); 复制代码

关于怎么使用体系结构的扩展,请参考以下集合链接和博客文章

结尾

Store 跟 Router 必须使用同一个 history 物件,否则会有其中一方不能正常工作,如果以后有遇到必須要先检查一次才行,记录一下。针对以上操作尝试梳理了一个简单 demo 大家可以查看 github。

如果你有任何想法欢迎直接「留言?」与我交流,那将是我进步的动力!

参考

  • React 应用架构设计
  • 浅析 Redux 的 store enhancer
  • createStore
  • applyMiddleware
  • combineReducers
  • compose
  • [译]简明 React Router v4 教程
  • React Router 与 Redux 整合
  • 模块热替换(hot module replacement)
  • react-router4 基于 react-router-config 的路由拆分与按需加载
  • React Router 4 简介及其背后的路由哲学
  • 异步 Action
  • redux 中间件之 redux-thunk
  • Redux 入门教程(二):中间件与异步操作
  • segmentfault.com/q/101000001…
  • openbase.io/js/connecte…
  • medium.com/@notrab/get…
退出移动版