(转载)原文链接: https://juejin.im/post/5dcaaa276fb9a04a965e2c9b#heading-18
前言
React
是单向数据流,数据通过 props
从父节点传递到子节点。如果顶层的某个 props
改变了, React
会重新渲染所有的子节点。注意⚠️:props
是只读的(即不可以使用 this.props
直接修改 props
),它是用于在整个组件树中传递数据和配置。
每个组件都有属于自己的 state
,state
和 props
的区别在于 state
只存在于组件内部。注意 ⚠️:只能从当前组件调用 this.setState
方法修改 state
值(不可以直接修改 this.state
)。
可见,更新子组件有两种方式,一种是改变子组件自身的 state
值,另一种则是更新子组件从父组件接收到的 this.props
值从而达到更新。
在 React
项目开发过程中,我们大多时候需要让组件共享某些数据。一般来说,我们可以通过在组件间传递数据(通过 props
)的方式实现数据共享,然而,当数据需要在非父子关系的组件间传递时操作起来则变得十分麻烦,而且容易让代码的可读性降低,这时候我们就需要使用 state
(状态)管理工具。
常见的状态管理工具有 redux
,mobx
。由于 redux
提供了状态管理的整个架构,并有着清晰的约束规则,适合在大型多人开发的应用中使用。本文介绍的是如何在 React
项目中使用 redux
进行状态管理。
进入正题
本节主要介绍 redux
和 react-router
相关的基础知识 和相关配置 。
redux
基本概念
redux
适用于多交互、多数据源的场景。从组件角度看,如果我们的应用有以下场景,则可以考虑在项目中使用 redux
:
- 某个组件的状态,需要共享
- 某个状态需要在任何地方都可以拿到
- 一个组件需要改变全局状态
- 一个组件需要改变另一个组件的状态
当我们的应用符合以上提到的场景时,若不使用 redux
或者其他状态管理工具,不按照一定规律处理状态的读写,项目代码的可读性将大大降低,不利于团队开发效率的提升。
如上图所示,redux
通过将所有的 state
集中到组件顶部,能够灵活的将所有 state
各取所需地分发给所有的组件。
redux
的三大原则:
- 整个应用的
state
都被存储在一棵object tree
中,并且object tree
只存在于唯一的store
中(这并不意味使用redux
就需要将所有的state
存到redux
上,组件还是可以维护自身的state
)。 state
是只读的。state
的变化,会导致视图(view
)的变化。用户接触不到state
,只能接触到视图,唯一改变state
的方式则是在视图中触发action
。action
是一个用于描述已发生事件的普通对象。- 使用
reducers
来执行state
的更新。reducers
是一个纯函数,它接受action
和当前state
作为参数,通过计算返回一个新的state
,从而实现视图的更新。
如上图所示,redux
的工作流程大致如下:
- 首先,用户在视图中通过
store.dispatch
方法发出action
。 - 然后,
store
自动调用reducers
,并且传入两个参数:当前state
和收到的action
。reducers
会返回新的state
。 - 最后,当
store
监听到state
的变化,就会调用监听函数,触发视图的重新渲染。
放一张图加深理解 ⚡⚡️⚡️️:
API
store
store
就是保存数据的地方,整个应用只能有一个 store
。
redux
提供 createStore
这个函数,用来创建一个 store
以存放整个应用的 state
:
import { createStore } from 'redux'; const store = createStore(reducer, [preloadedState], enhancer);
可以看到,createStore
接受 reducer
、初始 state
(可选)和增强器作为参数,返回一个新的 store
对象。
state
store
对象包含所有数据。如果想得到某个时点的数据,就要对 store
生成快照。这种时点的数据集合,就叫做 state
。
如果要获取当前时刻的 state
,可以通过 store.getState()
方法拿到:
import { createStore } from 'redux'; const store = createStore(reducer, [preloadedState], enhancer); const state = store.getState();
action
state
的变化,会导致视图的变化。但是,用户接触不到 state
,只能接触到视图。所以,state
的变化必须是由视图发起的。
action
就是视图发出的通知,通知store
此时的 state
应该要发生变化了。
action
是一个对象。其中的 type
属性是必须的,表示 action
的名称。其他属性可以自由设置,社区有一个规范可以参考:
const action = { type: 'ADD_TODO', payload: 'Learn Redux' // 可选属性 };
上面代码定义了一个名称为 ADD_TODO
的 action
,它携带的数据信息是 Learn Redux
。
Action Creator
view
要发送多少种消息,就会有多少种 action
,如果都手写,会很麻烦。
可以定义一个函数来生成 action
,这个函数就称作 Action Creator
,如下面代码中的 addTodo
函数:
const ADD_TODO = '添加 TODO'; function addTodo(text) { return { type: ADD_TODO, text } } const action = addTodo('Learn Redux');
redux-actions
是一个实用的库,让编写 redux
状态管理变得简单起来。该库提供了 createAction
方法用于创建动作创建器:
import { createAction } from "redux-actions" export const INCREMENT = 'INCREMENT' export const increment = createAction(INCREMENT)
上边代码定义一个动作 INCREMENT
, 然后通过 createAction
创建了对应 Action Creator
:
调用 increment()
时就会返回 { type: 'INCREMENT' }
调用increment(10)
返回 { type: 'INCREMENT', payload: 10 }
store.dispatch()
store.dispatch()
是视图发出 action
的唯一方法,该方法接受一个 action
对象作为参数:
import { createStore } from 'redux'; const store = createStore(reducer, [preloadedState], enhancer); store.dispatch({ type: 'ADD_TODO', payload: 'Learn Redux' });
结合 Action Creator
,这段代码可以改写如下:
import { createStore } from 'redux'; import { createAction } from "redux-actions" const store = createStore(reducer, [preloadedState], enhancer); const ADD_TODO = 'ADD_TODO'; const add_todo = createAction('ADD_TODO'); // 创建 Action Creator store.dispatch(add_todo('Learn Redux'));
reducer
store
收到 action
以后,必须给出一个新的 state
,这样视图才会进行更新。state
的计算(更新)过程则是通过 reducer
实现。
reducer
是一个函数,它接受 action
和当前 state
作为参数,返回一个新的 state
:
const reducer = function (state, action) { // ... return new_state; };
为了实现调用 store.dispatch
方法时自动执行 reducer
函数,需要在创建 store
时将将 reducer
传入 createStore
方法:
import { createStore } from 'redux'; const reducer = function (state, action) { // ... return new_state; }; const store = createStore(reducer);
上面代码中,createStore
方法接受 reducer
作为参数,生成一个新的 store
。以后每当视图使用 store.dispatch
发送给 store
一个新的 action
,就会自动调用 reducer
函数,得到更新的 state
。
redux-actions
提供了 handleActions
方法用于处理多个 action
:
// 使用方法: // handleActions(reducerMap, defaultState) import { handleActions } from 'redux-actions'; const initialState = { counter: 0 }; const reducer = handleActions( { INCREMENT: (state, action) => ({ counter: state.counter + action.payload }), DECREMENT: (state, action) => ({ counter: state.counter - action.payload }) }, initialState, );
拆分、合并 reducer
前面提到,在一个 react
应用中只能有一个 store
用于存放应用的 state
。组件通过调用 action
函数,传递数据到 reducer
,reducer
根据数据更新对应的 state
。
对于大型应用来说,应用的 state
必然十分庞大,导致 reducer
的复杂度也随着变大。
拆分
在这个时候,就可以考虑将 reducer
拆分成多个单独的函数,让每个函数负责独立管理 state
的一部分。
合并
redux
提供了 combineReducers
辅助函数,可将独立分散的 reducer
合并成一个最终的 reducer
函数,然后在创建 store
时作为 createStore
的参数传入。
我们可以根据业务需要,把所有子 reducer
放在不同的目录下,然后在在一个文件里面统一引入,最后将合并后的 reducer
导出:
// src/model/reducers.ts import { combineReducers } from 'redux'; import UI from './UI/reducers'; import user from './user/reducers'; import content from './content/reducers'; const rootReducer = combineReducers({ UI, user, content, }); export default rootReducer;
中间件及异步操作
对 redux
而言,同步指的是当视图发出 action
以后,reducer
立即算出 state
(原始的redux
工作流程),而异步指的是在 action
发出以后,过一段时间再执行 reducer
。
同步通常发生在原生 redux
的工作流程中,而在大多数实际场景中,更多的是需要异步操作:action
发出以后,在进入 reducer
之前需要先完成一个异步任务,比如发送 ajax
请求后拿到数据后,再进入 reducer
执行计算并对 state
进行更新。
显然原生的 redux
是不支持异步操作的,这就要用到新的工具——中间件(middleware)来处理这种业务场景。从本质上来讲中间件是对 store.dispatch
方法进行了拓展。
中间件提供位于 action
发起之后,到达 reducer
之前的扩展点:即通过 store.dispatch
方法发出的 action
会依次经过各个中间件,最终到达 reducer
。
我们可以利用中间件来进行日志记录(redux-logger
)、创建崩溃报告(自己写crashReporter
)、调用异步接口(redux-saga
)或者路由(connected-react-router
)等操作。
redux
提供了一个原生的 applyMiddleware
方法,它的作用是将所有中间件组成一个数组,依次执行。假如要使用 redux-logger
来实现日志记录功能,用法如下:
import { applyMiddleware, createStore } from 'redux'; import createLogger from 'redux-logger'; const logger = createLogger(); const store = createStore( reducer, applyMiddleware(logger) );
如果有多个中间件,则将中间件依次作为参数传入 applyMiddleware
方法中:
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger'; import createSagaMiddleware from 'redux-saga'; const logger = createLogger(); // 日志记录 const sagaMiddleware = createSagaMiddleware(); // 调用异步接口 let middleware = [sagaMiddleware]; middleware.push(logger); const store = createStore( reducer, // 可传initial_state applyMiddleware(...middleware) );
需要注意⚠️的是:
createStore
方法可以接受整个应用的初始状态作为参数(可选),若传入初始状态,applyMiddleware
则需要作为第三个参数。
有的中间件有次序要求,使用前要查一下文档(如 redux-logger
一定要放在最后,否则输出结果会不正确)。
react-redux
概念介绍
前面小节介绍的 redux
本身是一个可以结合 react
,vue
,angular
甚至是原生 javaScript
应用使用的状态库。
为了让 redux
帮我们管理 react
应用的状态,需要把 redux
与 react
连接,官方提供了 react-redux库(这个库是可以选用的,也可以只用redux
)。
react-redux
将所有组件分成 UI 组件和容器组件两大类:
UI 组件只负责 UI 的呈现,不含有状态(this.state
),所有数据都由 this.props
提供,且不使用任何 redux
的 API。
容器组件负责管理数据和业务逻辑,含有状态(this.state
),可使用 redux
的 API。
简而言之,容器组件作为 UI 组件的父组件,负责与外部进行通信,将数据通过 props
传给 UI 组件渲染出视图。
react-redux
规定,所有的 UI 组件都由用户提供,容器组件则是由 react-redux
自动生成。也就是说,用户负责视觉层,状态管理则是全部交给 react-redux
。
API
connect 方法
react-redux
提供了 connect
方法,用于将 UI 组件生成容器组件:
import { connect } from 'react-redux' class Dashboard extends React.Component { ... // 组件内部可以获取 this.props.loading 的值 } const mapStateToProps = (state) => { return { loading: state.loading, } } // 将通过 connect 方法自动生成的容器组件导出 export default connect( mapStateToProps, // 可选 // mapDispatchToProps, // 可选 )(Dashboard)
从上面代码可以看到,connect
方法接受两个可选参数用于定义容器组件的业务逻辑:
mapStateToProps
负责输入逻辑,即将state
映射成传入 UI 组件的参数(props
)mapDispatchToProps
负责输出逻辑,即将用户对 UI 组件的操作映射成action
注意⚠️:当connect
方法不传入任何参数时,生成的容器组件只可以看作是对 UI 组件做了一个单纯的包装,不含有任何的业务逻辑:
省略 mapStateToProps
参数, UI 组件就不会订阅 store
,即 store
的更新不会引起 UI 组件的更新。
省略 mapDispatchToProps
参数, UI 组件就不会将用户的操作当作 action
发送数据给 store
,需要在组件中手动调用 store.dispatch
方法。
mapStateToProps
mapStateToProps
是一个函数,它的作用就是建立一个从 state
对象(外部)到 UI 组件 props
对象的映射关系。该函数会订阅 整个应用的 store
,每当 state
更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。
mapStateToProps
的第一个参数总是 state
对象,还可以使用第二个参数(可选),代表容器组件的 props
对象:
// 容器组件的代码 // <Dashboard showType="SHOW_ALL"> // All // </Dashboard> const mapStateToProps = (state, ownProps) => { return { active: ownProps.showType === "SHOW_ALL", loading: state.loading, } }
使用 ownProps
作为参数后,如果容器组件的参数发生变化,也会引发 UI 组件重新渲染。
mapDispatchToProps
mapDispatchToProps
是 connect
函数的第二个参数,用来建立 UI 组件的参数到 store.dispatch
方法的映射。
由于在项目中大多使用 mapDispatchToProps
比较少,这里不进行细讲。关于mapStateToProps
、mapDispatchToProps
和 connect
的更详细用法说明可以查看文档。
Provider 组件
使用 connect
方法生成容器组件以后,需要让容器组件拿到 state
对象,才能生成 UI 组件 的参数。
react-redux
提供了 Provider
组件,可以让容器组件拿到 state
,具体用法是需要用 Provider
组件包裹项目的根组件(如App),使得根组件所有的子组件都可以默认获取到 state
对象:
import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import App from './App'; import { store } from './store/configureStore'; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root'), );
react-router
react-router
是完整的 react
的路由解决方案,它保持 UI
与 URL
的同步。在项目中我们使用的是最新的 v4
版。
需要注意⚠️的是,在开发中不应该直接安装 react-router
,因为:在 v4
版中 react-router
被划分为三个包:react-router
,react-router-dom
和 react-router-native
,它们的区别如下:
react-router
:提供核心的路由组件和函数。react-router-dom
:提供浏览器使用的路由组件和函数。react-router-native
:提供react-native
对应平台使用的路由组件和函数。
当我们的 react
应用同时使用了 react-router
和 redux
,则可以将两者进行更深度的整合,实现:
将 router
的数据与 store
进行同步,并且可以从 store
访问 router
数据,可使用 this.props.dispatch
方法发送 action
。
通过 dispatch actions
导航,个人理解是可使用 store.dispatch(push('routerName'))
切换路由。
在 redux devtools
中支持路由改变的时间旅行调试。
想要实现以上的目标,则可以通过 connected-react-router
和 history
两个库进行实现,步骤如下:
在创建 store
的文件添加配置,包括创建 history
对象、使用 connected-react-router
提供的 connectRouter
方法和 history
对象创建 root reducer
、使用 connected-react-router
提供的 routerMiddleware
中间件和 history
对象实现 dispatch actions
导航。
import { connectRouter, routerMiddleware } from 'connected-react-router'; import createHistory from 'history/createBrowserHistory'; import { createStore, applyMiddleware } from 'redux'; import { createLogger } from 'redux-logger'; import createSagaMiddleware from 'redux-saga'; import reducer from '../model/reducers'; export const history = createHistory(); const sagaMiddleware = createSagaMiddleware(); // 调用异步接口 let middleware = [sagaMiddleware, routerMiddleware(history)]; const logger = createLogger(); // 日志记录 middleware.push(logger); const initialState = {}; const store = createStore( connectRouter(history)(reducer), initialState, applyMiddleware(...middleware) );
在项目入口文件 index.js
中为根组件中添加配置,包括使用 connected-react-router
提供的 ConnectedRouter
组件包裹路由,将ConnectedRouter
组件作为Provider
的子组,并且将在 store
中创建的 history
对象引入,将其作为 props
属性 传入ConnectedRouter
组件:
import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { Provider } from 'react-redux' import { ConnectedRouter } from 'connected-react-router' import App from './App' import rootSaga from './model/sagas'; import { store, history } from './store/configureStore'; ReactDOM.render( <Provider store={store}> <ConnectedRouter history={history}> <App /> </ConnectedRouter> </Provider>, document.getElementById('root'), );
以上则完成了 react-router
和 redux
的深度整合 ✌️。
如果这分享对您有帮助,关注一下再走吧