redux
在我们开发过程中,很多时候,我们需要让组件共享某些数据,虽然可以通过组件传递数据实现数据共享,但是如果组件之间不是父子关系的话,数据传递是非常麻烦的,而且容易让代码的可读性降低,这时候我们就需要一个 state(状态)管理工具。常见的状态管理工具有 redux,mobx,这里选择 redux 进行状态管理。值得注意的是 React 16.3 带来了全新的Context API,我们也可以使用新的 Context API 做状态管理。Redux 是负责组织 state 的工具,但你也要考虑它是否适合你的情况。
在下面的场景中,引入 Redux 是比较明智的:
- 你有着相当大量的、随时间变化的数据
- 你的 state 需要有一个单一可靠数据来源
- 你觉得把所有 state 放在最顶层组件中已经无法满足需要了
的确,这些场景很主观笼统。因为对于何时应该引入 Redux 这个问题,对于每个使用者和每个应用来说都是不同的。
对于 Redux 应该如何、何时使用的更多建议,请看:
Redux 的创造者 Dan Abramov 又补充了一句
"只有遇到 React 实在解决不了的问题,你才需要 Redux 。"
react-redux
react-redux 提供Provider
组件通过 context 的方式向应用注入 store,然后组件使用connect
高阶方法获取并监听 store,然后根据 store state 和组件自身的 props 计算得到新的 props,注入该组件,并且可以通过监听 store,比较计算出的新 props 判断是否需要更新组件。
render(
,
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 来以存放应用中所有的 state
,createStore
的参数形式如下:
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 一起使用时大部分情况下都是正常的,但是偶尔会出现路由更新但是子路由或活动导航链接没有更新。这个情况发生在:
- 组件通过
connect()(Comp)
连接 redux。 - 组件不是一个“路由组件”,即组件并没有像
这样渲染。
这个问题的原因是 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-redux
、connected-react-router
和 history
两个库将 react-router
与 redux
进行深度整合实现。
官方文档中提到的是 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(
,
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 将异步操作的结果返回回来。
这里需要说明几点:
-
fetchPosts
返回了一个函数,而普通的 Action Creator 默认返回一个对象。 - 返回的函数的参数是
dispatch
和getState
这两个 Redux 方法,普通的 Action Creator 的参数是 Action 的内容。 - 在返回的函数之中,先发出一个
store.dispatch({type: SET_DEMO_DATA.PENDING})
,表示异步操作开始。 - 异步操作结束之后,再发出一个
store.dispatch({ type: SET_DEMO_DATA.SUCCESS, payload: response })
,表示操作结束。
但是有一个问题,store.dispatch
正常情况下,只能发送对象,而我们要发送函数,为了让store.dispatch
可以发送函数,我们使用中间件——redux-thunk。
引入 redux-thunk 很简单,只需要在创建 store 的时候使用applyMiddleware(thunk)
引入即可。
开发调试工具
开发过程中免不了调试,常用的调试工具有很多,例如redux-devtools-extension
,redux-devtools
,storybook
等。
注意,从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…