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 来以存放应用中所有的 state
,createStore
的参数形式如下:
createStore(reducer, [preloadedState], enhancer)
所以我们创建 store 的代码如下:
代码语言:txt复制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。 - 组件不是一个“路由组件”,即组件并没有像
<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-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
代码语言:txt复制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'))
最终结果如下:image.png
异步任务流管理
实现异步操作的思路
大部分情况下我们的应用中都是同步操作,即 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:
代码语言:txt复制{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
所以想要实现异步操作需要做到:
- 操作开始时,发出一个 Action,触发 State 更新为“正在操作”,View 重新渲染componentDidMount() {
store.dispatch(fetchPosts())
}在组件加载成功后,送出一个 Action 用来请求数据,这里的
fetchPosts
就是 Action Creator。fetchPosts 代码如下: - 操作结束后,再发出一个 Action,触发 State 更新为“操作结束”,View 再次重新渲染redux-thunk异步操作至少送出两个 Action,第一个 Action 跟同步操作一样,直接送出即可,那么如何送出第二个 Action 呢? 我们可以在送出第一个 Action 的时候送一个 Action Creator 函数,这样第二个 Action 可以在异步执行完成后自动送出。
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
使用
代码语言:txt复制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。
如果你有任何想法欢迎直接「留言