Redux 是 React 框架下的一款状态管理工具,可以实现多个组件之间的数据共享和传递。学习和掌握 Redux 以及周边生态可以使我们更好的进行 React 项目开发。下面我们就详细的讲述 Redux 在实际项目开发中的使用。
store
及reducer
// store/reducer.js
// 创建reducer
const defaultState = { a: "" };
export default (state = defaultState, action) => {
return state;
};
// store/index.js
// 创建store
import { createStore } from "redux";
import reducer from "./reducer.js";
const store = createStore(reducer);
export default store;
store
中的数据// todoList.js
this.state = store.getState();
action
请求修改store
中的数据// todoList.js
const action = {
type : "CHANGE_INPUT_VALUE",
a: 123;
}
store.dispatch( action )
store
收到修改请求后在reducer
中执行更新// reducer.js
const defaultState = { a : "" };
export default ( state = defaultState , action ) => {
if ( action.type === "CHANGE_INPUT_VALUE" ) {
const newState = JSON.parse(JSON.stringify( state );
newState.a = action.a;
return newState;
}
return state ;
}
store
数据发生变化后触发回调方法重新获取store
数据来更新组件数据。// todoList.js
// 监听store中数据发生变化
store.subscribe( this.handleStoreChange)
// 执行回调更新组件数据
handleStoreChange() {
this.setState(store.getState());
}
|- store
|- index.js
import { createStore } from "redux";
import reducer from "./reducer.js";
const store = createStore(reducer);
export default store;
|- reducer.js
import { CHANGE_INPUT_VALUE } from "actionTypes";
const defaultState = { inputValue: "" };
export default ( state = defaultState , action ) => {
if ( action.type === "CHANGE_INPUT_VALUE" ) {
const newState = JSON.parse(JSON.stringify( state );
newState. inputValue = action.inputValue;
return newState;
}
return state ;
}
|- actionTypes.js
export const CHANGE_INPUT_VALUE = "change_input_value";
|- actionCreators.js
import { CHANGE_INPUT_VALUE } from "actionTypes";
export const getInputChangeAction = (value) => {
type: CHANGE_INPUT_VALUE,
value
}
|- todoList.js
import React , { Component } from "react";
import store from "./store";
import { getInputChangeAction } from "./store/actionCreators";
class TodoList extends Component {
constructor( props ) {
super( props );
this.state = store.getState();
this.handleInputChange = this.handleInputChange.bind(this);
this.handleStoreChange = this.handleStoreChange.bind(this);
store.subscribe(this.handleStoreChange);
}
render() {
return (
<input
value={this.state.inputValue}
onChange={this.handleInputChange}/>
)
}
handleInputChange(e) {
const action = getInputChangeAction(e.target.value);
store.dispatch(action);
}
handleStoreChange() {
this.setState(store.getState());
}
}
为了近一步丰富Redux
生态,围绕着Redux
周边有很多第三方中间件。其中为了使Redux
更好的提供异步操作的支持,有两个比较常用的中间件Redux-thunk
和Redux-saga
。
在使用Redux-thunk
和Redux-saga
之前,一直有一个困扰我的地方,那就是:
我们为什么要在React
项目中使用类似于Redux-thunk
这样的中间件?
它能给我们带来什么好处?
Redux-thunk
官方仓库这样介绍:
With a plain basic Redux store, you can only do simple synchronous updates by dispatching an action. Middleware extend the store’s abilities, and let you write async logic that interacts with the store.Thunks are the recommended middleware for basic Redux side effects logic, including complex synchronous logic that needs access to the store, and simple async logic like AJAX requests.
意思是说:
基础的
Redux store
只能通过派发action
来同步的更新数据,中间件扩展了store
的能力,你可以书写异步逻辑来影响store
。在需要在store
中处理复杂的同步逻辑和异步逻辑的时候,Thunks
是被推荐的中间件。
这点解释并不能打消我的疑虑…
store
的数据的时候,我可以在组件中执行异步请求,然后再派发action
更新reducer
的数据即可,类似下面的操作:// component.js
import { getData } from './actionCreators';
import store from '../store/index.js'
// ......
componentDidMount() {
fetch('http://192.168.xx/getList').then(res => {
const { data } = res
const action = getData(data)
store.dispatch(action)
})
},
// actionCreators.js
export const getData = (value) => ({
type: 'GET_DATA',
value
});
actionCreators
中操作异步逻辑,然后通过参数的方式传递dispacth
。// component.js
import store from '../store/index.js'
import { getData } from './store/actionCreatores.js'
......
componentDidMount() {
store.getData(store.dispatch)
}
// actionCreators.js
export const getData = (dispatch) => {
return fetch('http://192.168.xx/getList').then(res => {
const { data } = res
const action = {
type: 'GET_DATA',
data
}
dispatch(action)
})
};
以上两种方式都可以实现更新store
中的状态state
。
既然如此,我为什么还需要使用Redux-thunk?
英文版:https://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34599594#34599594
中文你可以看这篇:http://www.xiaojichao.com/post/why-do-we-need-middleware-for-async-flow-in-redux.html
redux
就是因为数据共享的问题,如果需要使用数据的每个组件都执行一遍请求操作,一方面不利于调试,也明显违背了使用redux
的初衷。因此需要讲 UI 组件和action creator
进行剥离。action creator
进行了分离,但是 UI 组件首先知道要调用的对象是同步还是异步的,而且还需要根据不同的方式编写不同风格的代码(例如,传递同步参数等)。UI 组件并不关心 action creator 是否是异步的。UI 组件就像正常的去调用一个普通的操作,当然也可以使用 mapDispatchToProps 来简化代码。UI 组件也不知道 action creator 是怎么实现的,因此你可以切换各种异步实现方式(Redux Thunk, Redux Promise, Redux Saga),而且还不需要改组件的代码。
UI 组件就好比是我们,而redux
就好比是银行,通常我们将钱存进银行,需要的时候就去银行取,这就是同步更新数据操作。
但是,当有一天我们需要使用在境外银行存的钱,我们就没必要自己直接去境外银行取,因为可能涉及到比较多的手续会很麻烦。那么我们就可以将专业的事情交给专业的人去做,让银行之间进行划转,我们直接去使用的银行取就可以了。对于我们来说,并没有改变取钱的方式,而中间的工作都交给了银行取操作,也就是这里的action creator
的异步操作。我们根本不用关心银行使用的是什么方法,对应的就是 UI 组件不关心,在action creator
中是使用的是Redux-thunk
还是 Redux-saga
,它不需要改组件的代码。
说到这里,大家应该都明白了吧!所以Redux-thunk
和 Redux-saga
就该登场了!!
npm install redux-thunk -S
注意
:在配置redux-thunk
的时候如果安装官方文档配置,直接把thunk
放到createStore
里的第二个参数,如果配置了Redux Dev Tools
会发生冲突,如果想要同时使用,就需要引入增强函数compose
。
// store/index.js
// 1引入applyMiddleware和增强函数compose
import { createStore, applyMiddleware, compose } from "redux";
import reducer from "./reducer";
// 2引入redux-thunk
import thunk from "redux-thunk";
// 3使用compose创建增强函数
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
// 4传入thunk
const enhancer = composeEnhancers(applyMiddleware(thunk));
// 5把enhancer作为createStore的第二个参数传入
const store = createStore(reducer, enhancer);
export default store;
// actionCreatores.js
export const getData = () =>{
// dispatch 会作为参数自动传递进来
return (dispatch)=>{
axios.get('xxx/getData').then((res)=>{
const data = res.data
const action = {
type: 'GET_DATA',
data
}
dispatch(action)
})
}
}
// component.js
import store from '../store/index.js'
import { getData } from './store/actionCreatores.js'
....
componentDidMount() {
const action = getData()
store.dispatch(action)
}
Redux-thunk
中间件可以使我们以一种更加优雅的方式异步更新store
中的数据,在中小型项目中,非常好用,但是也不得不承认其存在的缺点,Redux-thunk
将同步异步更新数据的逻辑都放在creator
中操作,很容易形成面条式的代码,不方便管理。那么,我们可以使用另一种更加适合于大型项目异步更新数据的中间件 - Redux-saga
。
npm install redux-saga -S
store
文件夹下创建saga.js
文件单独管理异步操作.touch saga.js
// store/index.js
// 1引入applyMiddleware和增强函数compose
import { createStore, applyMiddleware, compose } from "redux";
import reducer from "./reducer";
// 2引入创建saga中间件的方法.并创建saga中间件
import createSagaMiddleware from "redux-saga";
const sagaMiddleware = createSagaMiddleware();
// 3引入saga.js文件
import mySagas from "./saga.js";
// 4使用compose创建增强函数
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
// 5传入saga中间件
const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware));
// 6把enhancer作为createStore的第二个参数传入
const store = createStore(reducer, enhancer);
// 7执行saga.js文件
sagaMiddleware.run(mySagas);
export default store;
action
到actionCreator
// component.js
import store from '../store/index.js'
import { getData } from '../store/actionCreator.js'
componentDidMount() {
const action = getData()
store.dispatch(action)
}
actionCreator
转发action
到saga
// actionTypes.js
// 获取数据
export const GET_DATA = "get_data";
// 更新数据
export const UPDATE_DATA = "update_data";
// actionCreator.js
import { GET_DATA, UPDATE_DATA } from "./actionTypes.js";
// 通知saga执行异步操作的action
export const getData = () => ({
type: GET_DATA,
});
// saga执行异步操作完毕,更新reducer数据的action
export const updateData = () => ({
type: UPDATE_DATA,
data,
});
saga
监听到GET_DATA
的action
后,获取异步数据,获取到数据后派发action
到reducer
更新数据// saga.js
// 引入takeEvery(监听action),put(派发action)
import { takeEvery, put } from "redux-saga/effects";
import { GET_DATA } from "./actionTypes.js";
import { reqData } from "../api/index.js";
import { updateData } from "../store/actionCreator.js";
// 异步获取数据方法
function* getDataList() {
// 执行异步操作
const res = yield reqData();
if (res.code === 200) {
const { data } = res;
// 获取到数据后,派发action到reducer更新数据
const action = updateData(data);
yield put(action);
}
}
function* mySaga() {
// 监听到GET_DATA的action后执行getDataList方法获取数据
yield takeEvery(GET_DATA, getDataList);
}
export default mySaga;
reducer
收到更新数据的action
后更新reducer
数据// reduce.js
import { UPDATE_DATA } from "./actionTypes.js";
const defaultState = {
list: [],
};
export default (state = defaultState, action) => {
if (action.type === UPDATE_DATA) {
const newState = JSON.parse(JSON.stringify(state));
newState.list = action.data;
return newState;
}
};
saga
分模块管理在实际项目中,可能有很多需要异步派发的action
,如果所有的saga
都写在同一个文件中很容易造成混乱,因此根据业务分模块管理就变得十分有必要,redux-saga
本身也提供了分模块管理的方法。
// store/sagas/index.js
// saga相关模块化引入
import { fork, all } from "redux-saga/effects";
// 异步逻辑模块文件引入
import { loginSagas } from "./modules/login.js";
import { userSagas } from "./modules/user.js";
import { businessSagas } from "./modules/business.js";
// 合并saga,单一进入点,一次启动所有Saga
export default function* rootSaga() {
yield all([
fork(loginSagas).default,
fork(userSagas).default,
fork(businessSagas).default,
]);
}
// store/index.js
import { createStore, applyMiddleware, compose } from "redux";
import reducer from "./reducer";
import createSagaMiddleware from "redux-saga";
const sagaMiddleware = createSagaMiddleware();
// 引入合并后的saga
import rootSaga from "./sagas/index";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware));
const store = createStore(reducer, enhancer);
// 执行saga
sagaMiddleware.run(rootSaga);
export default store;
Redux
作为React
的状态共享管理工具,其本身也存在使用缺陷,为了更好的使用Redux
,社区基于Redux
推出了更加方便易用的第三方库React-redux
。
store
和reducer
// store/index.js
import { createStore } from "redux";
import reducer from "./reducer";
const store = createStore(reducer);
export default store;
// store/reducer.js
const defaultState = {
inputValue: "",
};
export default (state = defaultState, action) => {
return state;
};
Provider
组件连接store
被Provider
组件包裹的组件都可以使用store
中的数据.
// 入口index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import store from './store'
import TodoList from './TodoList'
cosnt App = (
<Provider store = {store}>
<TodoList />
</Provider>
)
ReactDOM.render(App,document.getElementById('root'))
connect
方法将store
中的数据和映射到组件的props
connect
方法将store
的dispatch
方法映射到组件的props
// TodoList.js
import React, { Component } from 'react'
import store from '../store'
import connect from 'react-redux'
class TodoList extends Component {
return (
// store的state隐射到组件props之后,使用的时候直接是this.props.xxx
// store的dispacth映射到props后,也可以直接this.props.fn绑定方法
<input value = {this.props.value}
onChange={this.props.changeInputValue}/>
)
}
// 将store中的数据映射到组件,变成组件的props,参数state是store中的state
const mapStateToProps = (state) => {
return {
inputValue:state.inputValue
}
}
// 把store的dispatch映射到组件,变成组件的props,可以直接this.props.xx的方式调用
const mapDisptachToProps = (dispatch) => {
return {
changeInputValue(e) {
const action = {
type: 'change_input_value',
value: e.target.value
}
dispatch(action)
}
}
}
// 把store的state和dispatch和TodoList组件关联
export defalut connect(mapStateToProps,mapDisptachToProps)(TodoList)
store
接收到dispatch
的action
后,在reducer
中更新store
中的数据// store/reducer.js
const defaultState = {
inputValue: ''
}
export default (state = defaultState, action) => {
if (action.type === 'change_input_value') {
const newState = JSON.parse(JSON.stringify( state );
newState.inputValue = action.inputValue;
return newState;
}
return state
}
Reducer
分模块管理实际项目开发中,reducer
中可能要放很多的数据,如果都存放在一个reducer
中,很容易造成数据很多而难以维护,因此我们可以根据业务模板拆分成多个reducer
进行分开管理。
reducer
// header/reducer.js
const defaultState = {
a: "",
};
export default (state = defaultState, action) => {
// ...更新数据,需要带模块名
return state;
};
// footer/reducer.js
const defaultState = {
b: "",
};
export default (state = defaultState, action) => {
// ...更新数据,需要带模块名
return state;
};
reducer
// store/reducer.js
import { combineReducers } from 'redux'
import reducer as headerReducer from './header/reducer.js'
import reducer as footerReducer from './footer/reducer.js'
const reducer = combineReducers({
header: headerReducer,
footer: footerReducer
})
export default reducer;
分模块管理后,取值时需要带上模块名。
// TodoList.js
// ...
const mapStateToProps = (state) => {
return {
// 需要带上模块名才能正确取值
a: state.header.a,
};
};
// ...
reducer 分模块管理后,主要变更就是以上三点,其他步骤不变。
在Redux
中,我们修改store
中的数据的时候,不可以直接修改,而是需要先拷贝一个副本来修改,然后再返回副本。该方式虽然可以实现,但拷贝副本对性能有一定的损耗。此时,immutable.js
就该登场了。将reducer
中的普通数据对象转换为immutable
对象就可以直接修改数据而无需拷贝副本。
reducer
中普通数据对象转换为immutable
对象// reducer.js
import { fromJS } from "immutable";
// 转换
const defaultState = fromJS({
a: "",
});
// 获取数据
// TodoList.js
// ...
const mapStateToProps = (state) => {
return {
// state是根基级reducer的state,header模块已经是immutable对象,因此需要get()方法获取值
a: state.header.get("a"),
};
};
// ...
// 更新数据
// reducer.js
// ...
export default (state = defaultState, action) => {
if (action.type === "xxxx") {
// 这里的state是header模块的state,已经转换为immutable对象,可以使用set来更新值
return state.set("a", 123);
/* 同时改变多个值时可以使用merge
return state.merge({
a: xxx,
b: yyy
})
*/
}
return state;
};
使用immutable.js
虽然进一步完善了redux
,但是在获取store
中的数据的时候,因为是不同类型的对象取值方式不统一。如:state.header.get()
,state
是普通对象,通过点的方式取对象属性,header
是immutable
对象,通过get
方法取值。为了进一步统一和完善。redux-immutable
就诞生了。
reducer
时将根级reducer
的state
转换为immutable
对象// store/reducer.js
import { combineReducers } from "redux-immutable";
import { reducer as headerReducer } from "../header/store/reducer.js";
const reducer = combineReducers({
header: headerReducer,
});
export default reducer;
// TodoList.js
// ...
const mapStateToProps = (state) => {
return {
// state和header都被转化为了immutable对象,都可以使用get方法取值
a: state.get("header").get("a"),
// 或者使用getIn方法
// a: state.getIn(['header','a'])
};
};
// ...
下面会分别演示在类组件和函数式组件中的用法:
|- index.js // 入口js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import store from './store'
import User from './User'
cosnt App = (
<Provider store = {store}>
<User />
</Provider>
)
ReactDOM.render(App,document.getElementById('root'))
// 类组件中使用
|- User.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actionCreators } from '../store/user';
class User extends Component {
componentDidMount(){
this.props.getUserInfoData()
}
render() {
return (
<div>{this.props.userInfo.userName}</div>
)
}
}
const mapStateToProp = (state) => {
return {
// 006-监听到store中数据更新了,传递到UI组件,组件就可以拿到数据了
userInfo: state.getIn(['user','userInfo'])
}
}
const mapDispathToProps = (dispatch) => ({
getUserInfoData() {
// 001-UI组件派发获取数据的action
dispatch(actionCreators.getUserInfo())
}
})
export default connect(mapStateToProps, mapDispathToProps)(User);
// 函数式组件中使用
|- User.js
import React,{ useEffect } from 'react';
import { useSelector } from 'react-redux';
import { actionCreators } from '../store/user';
function User(){
// 006-监听到store中数据更新了,传递到UI组件,组件就可以拿到数据了
const userInfo = useSelector((state) => {
return state.getIn(['user', 'userInfo']);
})
useEffect(() => {
// 001-UI组件派发获取数据的action
store.dispatch(actionCreators.getUserInfo());
},[])
return (
<div>{userInfo.userName}</div>
)
}
export default User;
|- store
|- modules
|- user
|- actionTypes.js
export const GET_USER_INFO = 'user/GET_USER_INFO';
export const SET_USER_INFO = 'user/SET_USER_INFO';
|- actionCreators.js
import * as actionTyps from './actionTyps';
// 002-接收UI组件获取用户信息的action,转发到saga
export const getUserInfo = () => ({
type: actionTyps.GET_USER_INFO,
});
// 接收saga更新用户信息的action,转发到reducer
export const setUserInfo = (payload) => ({
type: actionTyps.SET_USER_INFO,
data: payload,
});
|- saga.js
import { takeEvery, put } from 'redux-saga/effects';
import { queryUserInfo } from '@/api/api-user';
import { setUserInfo } from './actionCreators';
import * as actionTypes from './actionTypes';
// 异步获取用户信息
function* getUserInfos() {
const res = yield queryUserInfo();
if (res.code === '0') {
const { data } = res;
// 004- 获取到异步数据后派发action到reducer,更新数据
const action = setUserInfo(data);
yield put(action);
}
}
function* userSagas() {
// 003- 监听到转发过来的GET_USER_INFO的action,执行异步方法getUserInfos
yield takeEvery(actionTypes.GET_USER_INFO, getUserInfos);
}
export default businessSagas;
|- reducer.js
import * as actionTyps from "./actionTypes";
const defaultState = {
userInfo: {}
};
export default ( state = defaultState , action ) => {
switch(action.type) {
// 005-接收到saga更新数据的action后,执行更新数据操作
case actionTyps.SET_USER_INFO:
return state.set('userInfo',action.data)
default:
break;
}
return state;
}
|- index.js
import reducer from './reducer';
import * as actionCreators from './actionCreators';
import * as actionTypes from './actionTypes';
import * as userSagas from './sagas';
export { reducer, actionCreators, actionTypes, userSagas };
|- reducers.js
import { combineReducers } from 'redux-immutable';
import { reducer as userReducer } from './modules/user'
const reducer = combineReducers({
user: userReducer
})
export default reducer;
|- sagas.js
import { all, fork } from 'redux-saga/effects';
import { userSagas } from './modules/user';
// 合并saga,单一进入点,一次启动所有Saga
export default function* rootSaga() {
yield all([fork(userSagas.default)]);
}
|- index.js
import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import reducer from './reducers';
// 引入合并后的saga
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware));
const store = createStore(reducer, enhancer);
// 执行saga
sagaMiddleware.run(rootSaga);
export default store;
至此,使用Redux
作为React
的状态管理工具的分享就结束了,本文可谓是手把手的Redux
及周边生态教学,希望看到的你能有所裨益!