Redux 官方文档对 Redux 的定义是:一个可预测的 JavaScript 应用状态管理容器
安装
1
| npm install --save redux
|
Store
Redux应用只有单一的 store
通过createStore()
创建一个store
1 2 3
| import { createStore } from 'redux' import todoApp from './reducers' let store = createStore(todoApp)
|
createStore()
的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。
提供四个函数
getState()
方法获取 state
dispatch(action)
方法分发 action 更新 state
subscribe(listener)
注册监听器,返回的函数注销监听器
replaceReducer(nextReducer)
一般在 Webpack Code-Splitting 按需加载的时候用
redux规定禁止直接修改 state,也就是下面的写法
1 2
| var state = store.getState() state.counter = state.counter + 1
|
只能通过 dispatch 一个 action 来修改
Action
是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch()
将 action 传到 store。
1 Action格式
除了type
字段外,action的结构完全由你自己决定,一般参照 Flux 标准 Action 获取关于如何构造 action 的建议。
1 2 3 4 5 6
| const ADD_TODO = 'ADD_TODO'
{ type: ADD_TODO, text: 'Build my first Redux app' }
|
2 Action创建函数
就是生成action的方法
1 2 3 4 5 6
| function addTodo(text) { return { type: ADD_TODO, text } }
|
3 dispatch
只需把 action 创建函数的结果传给 dispatch()
方法即可发起一次 dispatch 过程
Reducer
指定了应用状态的变化如何响应 actions 并发送状态到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。
reducer就是一个纯函数,接收旧的 state 和 action,返回新的 state。
1
| ;(previousState, action) => newState
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| var initState = { counter: 0, todos: [] }
function reducer(state, action) { if (!state) state = initState
switch (action.type) { case 'ADD_TODO': var nextState = _.cloneDeep(state) nextState.todos.push(action.payload) return nextState case 'ADD_COUNT': return Object.assign({}, state, { visibilityFilter: action.filter }) default: return state } }
|
注意:
- 不要修改
state
。 使用 Object.assign()
新建了一个副本。不能这样使用 Object.assign(state, { visibilityFilter: action.filter })
,因为它会改变第一个参数的值
- 在
default
情况下返回旧的 state
。否则 state 会变成undefined
拆分Reducer
当业务逻辑复杂时,所有状态聚合在一个reducer函数里处理,逻辑会变得相当复杂。
我们可以提出一个主reducer函数,它调用多个子 reducer 分别处理子 state 中的数据,然后再把这些数据合成一个大的单一对象。
主 reducer 并不需要设置初始化时完整的 state。初始时,如果传入 undefined
, 子 reducer 将负责返回它们的默认值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 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 } }
function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default: return state } }
function todoApp(state = {}, action) { return { visibilityFilter: visibilityFilter(state.visibilityFilter, action), todos: todos(state.todos, action) } }
|
可以利用combineReducers()
来简化代码
1 2 3 4 5 6 7 8
| import { combineReducers } from 'redux'
const todoApp = combineReducers({ visibilityFilter, a: todos })
export default todoApp
|
注意上面的写法和下面完全等价:
1 2 3 4 5 6
| export default function todoApp(state = {}, action) { return { visibilityFilter: visibilityFilter(state.visibilityFilter, action), todos: todos(state.todos, action) } }
|
异步数据流
如果只是简单的redux store是不支持用dispatch异步更新store,可以使用react-thunk
来增强
安装
使用applyMiddleware引入中间件
与Koa和Express类似,redux也提供了注册中间件的方法:applyMiddleware
,这个中间件执行时间是在dispatch一个action之后,到达reducer之前,执行顺序是从上到下,传递action
1 2 3 4 5 6 7 8 9 10
| import thunk from 'redux-thunk' import { createStore, applyMiddleware } from 'redux' import rootReducer from './reducers'
const store = createStore( rootReducer, applyMiddleware( thunk, ) )
|
动机
引入后允许我们dispatch
一个函数,这个函数内部可以dispatch
,这个函数接受两个参数,第一个是dispatch
,第二个是getState
,函数内部允许放一些异步操作,来解决redux只能同步dispatch(action)
的问题
react-thunk 源码
出乎意料的简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => (next) => (action) => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); }
return next(action); }; }
const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
|
当然我们也可以用redux-promise
这个中间件来dispatch
一个promise
redux-promise 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import isPromise from 'is-promise'; import { isFSA } from 'flux-standard-action';
export default function promiseMiddleware({ dispatch }) { return next => action => { if (!isFSA(action)) { return isPromise(action) ? action.then(dispatch) : next(action); }
return isPromise(action.payload) ? action.payload .then(result => dispatch({ ...action, payload: result })) .catch(error => { dispatch({ ...action, payload: error, error: true }); return Promise.reject(error); }) : next(action); }; }
|
与react配合使用
安装
1
| npm install --save react-redux
|
使用方式
利用 Provider 组件分发store状态
1 2 3 4 5 6 7 8 9 10
| import store from './app/store' import { Provider } from 'react-redux'
ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
|
在组件中使用useSelector
,useDispatch
来获取 state 和分发 action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { useSelector, useDispatch } from 'react-redux' import { decrement, increment } from './counterSlice'
export function Counter() { const count = useSelector((state) => state.counter.value) const dispatch = useDispatch()
return ( <div> <button onClick={() => dispatch(increment())}> Increment </button> <span>{count}</span> <button onClick={() => dispatch(decrement())}> Decrement </button> </div> ) }
|
或者用connect
函数包裹组件,将state和dispatch映射到props中
1
| connect(mapStateToProps, mapDispatchToProps)(MyComponent)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| const mapStateToProps = (state) => { return { foo: state.bar } }
const mapDispatchToProps = (dispatch) => { return { onClick: () => { dispatch({ type: 'increatment' }); } }; }
class Foo extends Component { constructor(props){ super(props); } render () { return ( <> <div>this.props.foo</div> <button onClick = {this.props.onClick}>点击increase</button> </> ) } } Foo = connect(mapStateToProps, mapDispatchToProps)(Foo); export default Foo;
|
同步react-router
安装
1
| npm install --save connected-react-router
|
使用
1.创建一个以history作为参数,返回根reducer的函数
1 2 3 4 5 6 7 8 9
| import { combineReducers } from 'redux' import { connectRouter } from 'connected-react-router'
const createRootReducer = (history) => combineReducers({ router: connectRouter(history), ... }) export default createRootReducer;
|
2.创建一个history对象,将这个对象给上述的reducer和createRootReducer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { createBrowserHistory } from 'history' import { applyMiddleware, compose, createStore } from 'redux' import { routerMiddleware } from 'connected-react-router' import createRootReducer from './reducers'
export const history = createBrowserHistory()
export default function configureStore(preloadedState) { const store = createStore( createRootReducer(history), preloadedState, compose( applyMiddleware( routerMiddleware(history), ), ), )
return store }
|
3.利用 ConnectedRouter组件,传递给他history对象作为prop,并将该组件作为 react-redux的Provider组件的子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { Provider } from 'react-redux' import { Route, Switch } from 'react-router' import { ConnectedRouter } from 'connected-react-router' import configureStore, { history } from './configureStore' const store = configureStore()
ReactDOM.render( <Provider store={store}> <ConnectedRouter history={history}> { /* place ConnectedRouter under Provider */ } <> <Switch> <Route exact path="/" render={() => (<div>Match</div>)} /> <Route render={() => (<div>Miss</div>)} /> </Switch> </> </ConnectedRouter> </Provider>, document.getElementById('react-root') )
|
编程式导航
connect-react-router也提供了路由跳转的方法,比如 push 和 replace,但是这些方法只是创建了action,需要dispatch这些方法产生的action
1 2 3 4 5 6
| import {push, replace} from "connected-react-router";
function XXX() { dispatch(push("/page1")) }
|
参考
react学习笔记6-redux
一篇文章总结redux、react-redux、redux-saga
Redux中文文档