Redux 学习总结 (React)
在 React 的学习和开发中,如果 state (状态)变得复杂时(例如一个状态需要能够在多个 view 中使用和更新),使用 Redux 可以有效地管理 state,使 state tree 结构清晰,方便状态的更新和使用。
当然,Redux 和 React 并没有什么关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。只是对我来说目前主要需要在 React 中使用,所以在这里和 React 联系起来便于理解记忆。
数据流
Action
只是描述 state (状态)更新的动作,即“发生了什么”,并不更新 state。
const ADD_TODO = 'ADD_TODO'{type: ADD_TODO,text: 'Build my first Redux app'
}
- type:必填,表示将要执行的动作,通常会被定义成字符串常量,尤其是大型项目。
- 除了 type 外的其他字段:可选,自定义,通常可传相关参数。例如上面例子中的 text。
Action 创建函数
简单返回一个 Action:
function addTodo(text) {return {type: ADD_TODO,text}
}
dispatch Action:
dispatch(addTodo(text))
// 或者创建一个 被绑定的 action 创建函数 来自动 dispatch
const boundAddTodo = text => dispatch(addTodo(text))
boundAddTodo(text)
帮助生成 Action 创建函数的库(对减少样板代码有帮助):
- redux-actions
- redux-act
redux-actions
createAction(s)
、handleAction(s)
、combineActions
createAction(type,payloadCreator = Identity, // function/undefined/null,默认使用 lodash 的 Identity; 如果传入 Error,则不会调用 payloadCreator 处理 Error,而是设置 action.error 为 true?metaCreator // 用来保存 payload 以外的其他数据
)const addTodo = createAction('ADD_TODO',text => ({text: text.trim(), created_at: new Date().getTime()}),() => ({ admin: true })
);expect(addTodo('New Todo')).to.deep.equal({type: 'ADD_TODO',payload: {text: 'New Todo',created_at: 1551322911779},meta: { admin: true }
});const error = new TypeError('error');
expect(addTodo(error)).to.deep.equal({type: 'ADD_TODO',payload: error,error: true
});
createActions(actionMap, // {type => payloadCreator / [payloadCreator, metaCreator] / actionMap}?...identityActions, // 字符串类型的参数列表,表示一组使用 Identity payloadCreator 的 actions?options // 定义 type 前缀:{ prefix, namespace } prefix 前缀字符串,namespace 前缀和 type 之间的分隔符(默认为 /)
)const actionCreators = createActions({TODO: {ADD: todo => ({ todo }), // payload creatorREMOVE: [todo => ({ todo }), // payload creator(todo, warn) => ({ todo, warn }) // meta creator]},COUNTER: {INCREMENT: [amount => ({ amount }), amount => ({ key: 'value', amount })],DECREMENT: amount => ({ amount: -amount }),SET: undefined // given undefined, the identity function will be used}},'UPDATE_SETTINGS',{prefix: 'app',namespace: '-'}
);expect(actionCreators.todo.remove('Todo 1', 'warn: xxx')).to.deep.equal({type: 'app-TODO-REMOVE',payload: { todo: 'Todo 1' },meta: { todo: 'Todo 1', warn: 'warn: xxx' }
});expect(actionCreators.updateSettings({ theme: 'blue' })).to.deep.equal({type: 'app-UPDATE_SETTINGS',payload: { theme: 'blue' }
})
redux-actions 也能帮助生成 reducer,
handleAction(type,reducer | reducerMap = Identity,defaultState,
)handleAction('ADD_TODO',(state, action) => ({...state,{text: action.payload.text,completed: false}}),{ text: '--', completed: false },
);const reducer = handleAction('INCREMENT', {next: (state, { payload: { amount } }) => ({ ...state, counter: state.counter + amount }),throw: state => ({ ...state, counter: 0 }),
}, { counter: 10 });expect(reducer(undefined, increment(1)).to.deep.equal({ counter: 11 });
expect(reducer({ counter: 5 }, increment(1)).to.deep.equal({ counter: 6 });
expect(reducer({ counter: 5 }, increment(new Error)).to.deep.equal({ counter: 0 });
handleActions(reducerMap, defaultState[, options])handleActions({INCREMENT: (state, action) => ({counter: state.counter + action.payload}),DECREMENT: (state, action) => ({counter: state.counter - action.payload})},{ counter: 0 }
);// Map
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';handleActions(new Map([[INCREMENT,(state, action) => ({counter: state.counter + action.payload})],[DECREMENT,(state, action) => ({counter: state.counter - action.payload})]]),{ counter: 0 }
);const increment = createAction(INCREMENT);
const decrement = createAction(DECREMENT);const reducer = handleActions(new Map([[increment,(state, action) => ({counter: state.counter + action.payload})],[decrement,(state, action) => ({counter: state.counter - action.payload})]]),{ counter: 0 }
);
当多个 action 有相同的 reducer 时,可以使用 combineActions,
combineActions(...types) // types: strings, symbols, or action creatorsconst { increment, decrement } = createActions({INCREMENT: amount => ({ amount }),DECREMENT: amount => ({ amount: -amount })
});const reducer = handleActions({[combineActions(increment, decrement)]: (state,{ payload: { amount } }) => {return { ...state, counter: state.counter + amount };}},{ counter: 10 }
);
Reducer
说明在发起 action 后 state 应该如何更新。
是一个纯函数:只要传入参数相同,返回计算得到的下一个 state 就一定相同。
(previousState, action) => newState
注意,不能在 reducer 中执行的操作:
- 修改传入的参数
- 执行有副作用的操作,如 API 请求和路由跳转
- 调用非纯函数,如 Date.now() 或 Math.random()
import { combineReducers } from 'redux'
import {ADD_TODO,TOGGLE_TODO,SET_VISIBILITY_FILTER,VisibilityFilters
} from './actions'
const { SHOW_ALL } = VisibilityFiltersfunction visibilityFilter(state = SHOW_ALL, action) {switch (action.type) {case SET_VISIBILITY_FILTER:return action.filterdefault:return state}
}function todos(state = [], action) {switch (action.type) {case ADD_TODO:return {...state,{text: action.text,completed: false}}case TOGGLE_TODO:return state.map((todo, index) => {if (index === action.index) {return Object.assign({}, todo, {completed: !todo.completed})}return todo})default:return state}
}const todoApp = combineReducers({visibilityFilter,todos
})export default todoApp
Store
Redux 应用只有一个单一的 store。
- 维持应用的 state;
- 提供 getState() 方法获取 state;
- 提供 dispatch(action) 方法更新 state;
- 通过 subscribe(listener) 注册监听器;
- 通过 subscribe(listener) 返回的函数注销监听器。
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp,[preloadedState], // 可选,state 初始状态enhancer
)
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import DevTools from './containers/DevTools'
import reducer from '../reducers/index'export default function configureStore() {const store = createStore(reducer,compose(applyMiddleware(thunk),DevTools.instrument()));return store;
}
react-redux
connect()
方法(mapStateToProps
、mapDispatchToProps
)
替代 store.subscribe()
,从 Redux state 树中读取部分数据,并通过 props 提供给要渲染的组件。
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from './actions';class App extends Component {handleAddTodo = () => {const { actions } = this.props;actions.addTodo('Create a new todo');}render() {const { todos } = this.props;return (<div><Button onClick={this.handleAddTodo}>+</Button><ul>{todos.map(todo => (<Todo key={todo.id} {...todo} />))}</ul></div>);}
}function mapStateToProps(state) {return {todos: state.todos};
}function mapDispatchToProps(dispatch) {return {actions: bindActionCreators({addTodo: actions.addTodo}, dispatch)}
}export default connect(mapStateToProps,mapDispatchToProps
)(App);
Provider
组件
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import configureStore from './store/configureStore'
import App from './components/App'render(<Provider store={configureStore()}><App /></Provider>,document.getElementById('root')
API 请求
一般情况下,每个 API 请求都需要 dispatch 至少三种 action:
- 通知 reducer 请求开始的 action
{ type: 'FETCH_POSTS_REQUEST' }
reducer 可能会{...state, isFetching: true}
- 一种通知 reducer 请求成功的 action
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
reducer 可能会{...state, isFetching: false, data: action.response}
- 一种通知 reducer 请求失败的 action
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
reducer 可能会{...state, isFetching: false, error: action.error}
使用 middleware 中间件实现网络请求:
- redux-thunk
- redux-saga
redux-thunk
通过使用指定的 middleware,action 创建函数除了返回 action 对象外还可以返回函数。这时,这个 action 创建函数就成为了 thunk。
function shouldFetchPosts(state) {if (state.posts.isFetching) {return false;}return true;
}export function fetchPosts() {return (dispatch, getState) => {if (!shouldFetchPosts(getState())) {return Promise.resolve();}dispatch({ type: 'FETCH_POSTS_REQUEST' });return fetch(postApi).then(response => {const data = response.json();return dispatch({type: 'FETCH_POSTS_SUCCESS', data});});}
}
...actions.fetchPosts().then(() => console.log(this.props.posts))...function mapStateToProps(state) {return {posts: state.posts};
}function mapDispatchToProps(dispatch) {return {actions: bindActionCreators({fetchPosts}, dispatch)}
}...
redux-saga
声明式 vs 命令式:
- DOM: jQuery / React
- Redux effects: redux-thunk / redux-saga
实现获取用户信息的两种方式对比:
redux-thunk
<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>function loadUserProfile(userId) {return dispatch => fetch(`http://data.com/${userId}`).then(res => res.json()).then(data => dispatch({ type: 'USER_PROFILE_LOADED', data }),err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })); }
redux-saga
<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div> function* loadUserProfileOnNameClick() {yield* takeLatest("USER_NAME_CLICKED", fetchUser); }function* fetchUser(action) {try {const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)yield put({ type: 'USER_PROFILE_LOADED', userProfile })} catch(err) {yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })} }
比较看来,使用 redux-saga 的代码更干净清晰,方便测试。
redux-saga 使用了 ES6 的 Generator 功能,让异步的流程更易于读取,写入和测试。
class UserComponent extends React.Component {...onSomeButtonClicked() {const { userId, dispatch } = this.propsdispatch({type: 'USER_FETCH_REQUESTED', payload: {userId}})}...
}
sagas.js
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...'// worker Saga: will be fired on USER_FETCH_REQUESTED actions
function* fetchUser(action) {try {const user = yield call(Api.fetchUser, action.payload.userId);yield put({type: "USER_FETCH_SUCCEEDED", user: user});} catch (e) {yield put({type: "USER_FETCH_FAILED", message: e.message});}
}
// or
function fetchUserApi(userId) {return Api.fetchUser(userId).then(response => ({ response })).catch(error => ({ error }))
}function* fetchUser(action) {const { response, error } = yield call(fetchUserApi, action.payload.userId);if (response) {yield put({type: "USER_FETCH_SUCCEEDED", user: user});} else {yield put({type: "USER_FETCH_FAILED", message: e.message});}
}/*Starts fetchUser on each dispatched `USER_FETCH_REQUESTED` action.Allows concurrent fetches of user.
*/
function* mySaga() {yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}/*Alternatively you may use takeLatest.Does not allow concurrent fetches of user. If "USER_FETCH_REQUESTED" getsdispatched while a fetch is already pending, that pending fetch is cancelledand only the latest one will be run.
*/
function* mySaga() {yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}export default mySaga;/**** 测试: ****/
const iterator = fetchUser({ payload: {userId: 123} })// 期望一个 call 指令
assert.deepEqual(iterator.next().value,call(Api.fetchUser, 123),"fetchProducts should yield an Effect call(Api.fetchUser, 123)"
)// 创建一个假的响应对象
const user = {}// 期望一个 dispatch 指令
assert.deepEqual(iterator.next(user).value,put({ type: 'USER_FETCH_SUCCEEDED', user }),"fetchProducts should yield an Effect put({ type: 'USER_FETCH_SUCCEEDED', user })"
)// 创建一个模拟的 error 对象
const error = {}// 期望一个 dispatch 指令
assert.deepEqual(iterator.throw(error).value,put({ type: 'USER_FETCH_FAILED', error }),"fetchProducts should yield an Effect put({ type: 'USER_FETCH_FAILED', error })"
)
main.js
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'import reducer from './reducers'
import mySaga from './sagas'// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(reducer,applyMiddleware(sagaMiddleware)
)// then run the saga
sagaMiddleware.run(mySaga)// render the application
路由跳转
一般使用 react-router,与 redux 无关。如果想要使用 redux 管理 route 状态,可以使用 connect-react-router (history -> store -> router -> components)
dva 框架
dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
通过 reducers, effects 和 subscriptions 组织 model:
User Dashboard 的 model 配置,
import * as usersService from '../services/users';export default {namespace: 'users',state: {list: [],total: null,page: null,},reducers: {save(state, { payload: { data: list, total, page } }) {return { ...state, list, total, page };},},effects: {*fetch({ payload: { page = 1 } }, { call, put }) {const { data, headers } = yield call(usersService.fetch, { page });yield put({type: 'save',payload: {data,total: parseInt(headers['x-total-count'], 10),page: parseInt(page, 10),},});},*remove({ payload: id }, { call, put }) {yield call(usersService.remove, id);yield put({ type: 'reload' });},*patch({ payload: { id, values } }, { call, put }) {yield call(usersService.patch, id, values);yield put({ type: 'reload' });},*create({ payload: values }, { call, put }) {yield call(usersService.create, values);yield put({ type: 'reload' });},*reload(action, { put, select }) {const page = yield select(state => state.users.page);yield put({ type: 'fetch', payload: { page } });},},subscriptions: {setup({ dispatch, history }) {return history.listen(({ pathname, query }) => {if (pathname === '/users') {dispatch({ type: 'fetch', payload: query });}});},},
};
action 添加前缀 prefix,
function prefix(obj, namespace, type) {return Object.keys(obj).reduce((memo, key) => {const newKey = `${namespace}${NAMESPACE_SEP}${key}`;memo[newKey] = obj[key];return memo;}, {});
}function prefixNamespace(model) {const {namespace,reducers,effects,} = model;if (reducers) {if (isArray(reducers)) {model.reducers[0] = prefix(reducers[0], namespace, 'reducer');} else {model.reducers = prefix(reducers, namespace, 'reducer');}}if (effects) {model.effects = prefix(effects, namespace, 'effect');}return model;
}
reducer 处理,
function getReducer(reducers, state, handleActions) {// Support reducer enhancer// e.g. reducers: [realReducers, enhancer]if (Array.isArray(reducers)) {return reducers[1]((handleActions || defaultHandleActions)(reducers[0], state));} else {return (handleActions || defaultHandleActions)(reducers || {}, state);}
}
saga,
import * as sagaEffects from 'redux-saga/lib/effects';
import {takeEveryHelper as takeEvery,takeLatestHelper as takeLatest,throttleHelper as throttle,
} from 'redux-saga/lib/internal/sagaHelpers';
import { NAMESPACE_SEP } from './constants';function getSaga(effects, model, onError, onEffect) {return function*() {for (const key in effects) {if (Object.prototype.hasOwnProperty.call(effects, key)) {const watcher = getWatcher(key, effects[key], model, onError, onEffect);const task = yield sagaEffects.fork(watcher);yield sagaEffects.fork(function*() {yield sagaEffects.cancel(task);});}}};
}function getWatcher(resolve, reject, key, _effect, model, onError, onEffect) {let effect = _effect;let type = 'takeEvery';let ms;if (Array.isArray(_effect)) {// effect 是数组而不是函数的情况下暂不考虑}function *sagaWithCatch(...args) {try {yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });const ret = yield effect(...args.concat(createEffects(model)));yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });resolve(key, ret);} catch (e) {onError(e);if (!e._dontReject) {reject(key, e);}}}const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);switch (type) {case 'watcher':return sagaWithCatch;case 'takeLatest':return function*() {yield takeLatest(key, sagaWithOnEffect);};case 'throttle':return function*() {yield throttle(ms, key, sagaWithOnEffect);};default:return function*() {yield takeEvery(key, sagaWithOnEffect);};}
}function createEffects(model) {// createEffects(model) 的逻辑
}function applyOnEffect(fns, effect, model, key) {for (const fn of fns) {effect = fn(effect, sagaEffects, model, key);}return effect;
}
import { handleActions } from 'redux-actions';
import createSagaMiddleware from 'redux-saga/lib/internal/middleware';const prefixedModel = models.map(m => {return prefixNamespace({...m});
});
const reducers = {}, sagas = [];
for (const m of prefixedModel) {reducers[m.namespace] = getReducer(m.reducers,m.state,handleActions);if (m.effects)sagas.push(getSaga(m.effects, m, onError, onEffect));
}const sagaMiddleware = createSagaMiddleware();
sagas.forEach(sagaMiddleware.run)
react-coat
在掘金上看到一篇文章与DvaJS风云对话,是DvaJS挑战者?还是又一轮子?,发现了另一个 react 状态与数据流管理框架 react-coat,以下是代码示例:
// 仅需一个类,搞定 action、dispatch、reducer、effect、loading
class ModuleHandlers extends BaseModuleHandlers {@reducerprotected putCurUser(curUser: CurUser): State {return {...this.state, curUser};}@reducerpublic putShowLoginPop(showLoginPop: boolean): State {return {...this.state, showLoginPop};}@effect("login") // 使用自定义loading状态public async login(payload: {username: string; password: string}) {const loginResult = await sessionService.api.login(payload);if (!loginResult.error) {// this.updateState()是this.dispatch(this.actions.updateState(...))的快捷this.updateState({curUser: loginResult.data});Toast.success("欢迎您回来!");} else {Toast.fail(loginResult.error.message);}}// uncatched错误会触发@@framework/ERROR,监听并发送给后台@effect(null) // 不需要loading,设置为nullprotected async ["@@framework/ERROR"](error: CustomError) {if (error.code === "401") {// dispatch Action:putShowLoginPopthis.dispatch(this.actions.putShowLoginPop(true));} else if (error.code === "301" || error.code === "302") {// dispatch Action:路由跳转this.dispatch(this.routerActions.replace(error.detail));} else {Toast.fail(error.message);await settingsService.api.reportError(error);}}// 监听自已的INIT Action,做一些异步数据请求@effect()protected async ["app/INIT"]() {const [projectConfig, curUser] = await Promise.all([settingsService.api.getSettings(),sessionService.api.getCurUser()]);// this.updateState()是this.dispatch(this.actions.updateState(...))的快捷this.updateState({projectConfig,curUser,});}
}
参考资料:
- Redux 中文文档
- React Native Training
- Redux 中文文档
- Stackflow 上关于 redux-saga 的一个回答
Redux 学习总结 (React)相关推荐
- 【前端学习】React学习资料
React 是一种开源的 JavaScript 库,用于构建用户界面.它由 Facebook 开发并维护,已经成为了当今最流行的前端库之一.与其他框架不同,React 主要专注于视图层(View),旨 ...
- 学习使用React和Electron一次构建自己的桌面聊天应用程序
by Alex Booker 通过亚历克斯布克 学习使用React和Electron一次构建自己的桌面聊天应用程序 (Learn to build your own desktop chat app ...
- react开发模式_通过开发带有精灵动画的游戏来学习高级React模式
react开发模式 by Pavel Vlasov 通过帕维尔·弗拉索夫(Pavel Vlasov) 通过开发带有精灵动画的游戏来学习高级React模式 (Learn advanced React p ...
- [Redux/Mobx] 在React中你是怎么对异步方案进行选型的?
[Redux/Mobx] 在React中你是怎么对异步方案进行选型的? 小项目使用简单的redux-thunk方案,增加的代码量极少,只有两个api,上手成本低 大项目使用基于redux-saga的d ...
- Redux学习(一)——Redux的使用过程
一.为什么需要redux JavaScript开发的应用程序,已经变得越来越复杂了: JavaScript需要管理的状态越来越多,越来越复杂: 这些状态包括服务器返回的数据.缓存数据.用户操作产生的数 ...
- React学习(六)-React中组件的数据-state
虽互不曾谋面,但希望能和你成为笔尖下的朋友 以读书,技术,生活为主,偶尔撒点鸡汤 不作,不敷衍,意在真诚吐露,用心分享 点击左上方,可关注本刊 撰文 | 川川 ID:suibichuanji 点击下方 ...
- React学习(十)-React中编写样式CSS(styled-components)
虽互不曾谋面,但希望能和你成为笔尖下的朋友 以读书,技术,生活为主,偶尔撒点鸡汤 不作,不敷衍,意在真诚吐露,用心分享 点击左上方,可关注本刊 撰文 | 川川 VX-ID:suibichuanji 点 ...
- React学习(九)-React中发送Ajax请求以及Mock数据
虽互不曾谋面,但希望能和你成为笔尖下的朋友 以读书,技术,生活为主,偶尔撒点鸡汤 不作,不敷衍,意在真诚吐露,用心分享 点击左上方,可关注本刊 撰文 | 川川 VX-ID:suibichuanji 点 ...
- node.js学习之react,redux,react-redux
redux好难懂,终于明白了一点,做了个小demo,记录一下. 先看目录结构: src |--index.js |--actions |--index.js |--components |--Addi ...
最新文章
- 如何将CSS应用于iframe?
- C#动态属性(.NET Framework4.5支持)
- Maven中scope依赖范围和依赖的传递性
- MYSQL 去除重复 记录
- docker下交叉编译环境配置
- P1232 [NOI2013] 树的计数
- python时间去掉t_Python的set集合详解
- 判断sem信号量为零_将信号量递减为零的进程崩溃时,如何恢复信号量?
- arcgis字段计算器无法赋值_Arcgis空间连接工具的妙用
- linux nohup不生成日志,linux重定向及nohup不输出的方法
- 经典解释监视器和对象锁
- jq 实现头像(气泡式浮动)
- 注册双击Ctrl键 (DLL版)
- stc15f2k60f2单片机定时器_8 STC15F2K60S2单片机的定时器计数器 例题
- 分销系统开发 三级分销技术开发
- 【.net】C#如何发送电子邮件
- python for循环次数_Python for循环
- android 六边形简书,六边形RecyclerView
- 计算机专业在广东省的录取分数线,深圳大学广东本科批稳居广东高校前4!2019各省录取分数线发布!...
- 接受投资人投入材料一批_接受投资者投入材料的会计分录