Redux 学习笔记
Redux 简介
Redux 的作者为 Facebook 的 Dan Abramov 大神。Redux 是 JavaScript 状态容器,提供可预测化的状态管理。
Redux 并不是在项目中一定要用到的东西,相反,只有遇到 React 实在解决不了的问题你才需要用 Redux。
一般来说下面几种情况可能会用到 Redux:
- 用户的使用方式非常复杂
- 不同身份的用户有不同的使用方式(比如普通用户和管理员)
- 多个用户之间可以协作
- 与服务器大量交互,或者使用了 WebSocket
- View 要从多个来源获取数据
从组件的角度看,下面几种可以考虑使用 Redux:
- 某个组件的状态,需要共享
- 某个状态需要在任何地方都可以拿到
- 一个组件需要改变全局状态
- 一个组件需要改变另一个组件的状态
Redux 的特性
单一数据源
单一数据源(Single Source of Truth) 就是整个应用程序只存在于一个唯一的 store 中。
可预测性
改变 state 唯一的方法就是触发 action,可以用一个公式来表达就是 state + action = new state。
纯函数更新 Store
为了描述 action 如何更新 state,你需要编写 reducer,而 reducer 必须是纯函数,输出结果完全取决于它的输入参数,函数内部不依赖任何的外部依赖和外部资源。
Action
action 描述的一个行为的数据结构,它是 store 的唯一数据来源。
一般来说,action 就是 JavaScript 中的一个普通的对象,一般会有一个 type 属性来表示要执行的动作。
const ADD_TODO = 'ADD_TODO'
const action = {
type: ADD_TODO,
text: 'Build my first Redux app'
}
Action是view发出的通知,表示state应该要发生变化了。
Reducer
接收到 action 之后,进行相应的响应,以更新 state。
store 收到 Action 以后,必须给出一个新的 State,View 才会变化,这种 State 的计算过程就叫 Reducer。
Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。
Reducer 可以定义多个,每一个 Reducer 都可以接收到所有的 Action 。
function todoApp(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return { ...state, ...{ stateKey: action.newState } }
default:
return state
}
}
Store
Store 包含三个部分:State、Reducer、Dispatcher。
State 是真正的数据;Reducer 是处理 action 的,用来更新 State 的;Dispatcher 是用来派发 action 的。
View 上的一些操作会触发 Action,Action 传给 Dispatcher 去进行派发,给到 Reducer,Reducer 对 State 进行更新,State 更新之后就会在 View 上得到体现。整个过程是一个单向数据流。
核心 API
createStore(reducer)
创建 Store,需要传入一个 reducer 作为参数。
import { createStore } from 'redux'
import reducers from './reducers'
const store = createStore(reducers)
getState()
获取当前的 State 值。
store.getState()
dispatch(action)
派发 action,需要传入一个 action 作为参数,我们想要执行某些操作的时候就去 dispatch 相应的 action 就行。
// action creator
function action1() {
return { type: 'ACTION1', newState: Math.random() }
}
function action3() {
return { type: 'ACTION3', newState: Math.random() }
}
// store.dispatch({ type: 'ACTION1', newState: Math.random() })
// store.dispatch({ type: 'ACTION3', newState: Math.random() })
store.dispatch(action1())
store.dispatch(action3())
subscribe(listener)
订阅 store ,state 变化的时候会触发 执行 listener。
store.subscribe(() => console.log(store.getState()))
combineReducers({ reducer1, reducer2, ... })
把多个 Reducer 连接在一起形成一个新的 Reducer。
reducer1.js
export default function reducer1(state = { state1: 1, state2: 'abc' }, action) {
switch (action.type) {
case 'ACTION1':
return { ...state, ...{ state1: action.newState } }
case 'ACTION2':
return { ...state, ...{ state2: action.newState } }
default:
return state
}
}
reducer2.js
export default function reducer2(state = { state3: 3, state4: 'def' }, action) {
switch (action.type) {
case 'ACTION3':
return { ...state, ...{ state3: action.newState } }
case 'ACTION4':
return { ...state, ...{ state4: action.newState } }
default:
return state
}
}
reducer.js
import { combineReducers } from 'redux'
import reducer1 from './reducer1'
import reducer2 from './reducer2'
export default combineReducers({
reducer1,
reducer2,
})
bindActionCreators
此函数是为了简化 Action Creator 和 dispatch 函数使用的流程。
import { bindActionCreators, createStore } from 'redux'
// 前面的例子
// action creator
function action1() {
return { type: 'ACTION1', newState: Math.random() }
}
store.dispatch(action1())
// 等同于如下写法
function action1() {
return { type: 'ACTION1', newState: Math.random() }
}
// action 参数可以是一个对象,并可以传入多个 action creator
const action1Dispatch = bindActionCreators(action1, store.dispatch)
action1Dispatch()
connect
在 React 组件中想要使用 Redux,必须使用 connect 把,Redux 与 React 组件连接起来。connect 本质是一个高阶函数。
connect(mapStateToProps, mapDispatchToProps)(reactComponent): newReactComponent
传入本组件需要的 state 和 dispatch 函数,返回一个新的 React 组件。
import React from 'react'
import { connect, Provider } from 'react-redux'
...
class MyComponent1 extends React.Component {
// ...
}
// 此处要对 Store 中的 state 进行筛选
// 如果不筛选那么 Store 中的任何 state 更新都会导致组件刷新
// 这样会导致额外的性能开销
function mapStateToProps(state) {
return {
key: state.key,
...
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({ actionCreator1, actionCreator2, ... }, dispatch)
}
}
const ConnectedMyComponent1 = connect(mapStateToProps, mapDispatchToProps)(MyComponent1)
class MyComponent1WithStore extends React.Component {
render() {
return (
// Provider 下的组件都可以访问到 store
<Provider store={store}>
<ConnectedMyComponent1 />
</Provider>
)
}
}
Middleware 与异步操作
中间件就是截获某种类型的 action,比如对 ajax 请求,会进行一个预处理,然后形成最终的 action,发出真正的 action。其过程如下图所示:
中间件使用
使用 applyMiddlewares() 来将所有中间件组成一个数组,一次执行。
以 redux-logger 这个中间件为例。
import { applyMiddleware, createStore } from 'redux'
import { createLogger } from 'redux-logger'
const logger = createLogger()
const store = createStore(
reducer,
applyMiddleware(logger)
)
异步
同步操作只要发出一种 Action 即可,异步操作要发出三种 Action(开始、成功、失败)。
下面代码片段引入了异步操作,修改了 reducer 对 action 的处理,在异步操作中去 dispatch action。
function reducers(state = { count: 0, state: 'init' }, action) {
switch (action.type) {
case 'PLUS_START':
return { count: state.count, state: 'pending' }
case 'PLUS_SUCCESS':
return { count: state.count + 2, state: 'plus success' }
case 'PLUS_FAILED':
return { count: state.count, state: 'failed' }
...
default:
return state
}
}
function plusFailed() {
stop(1000, false).then(res => {
store.dispatch({ type: 'PLUS_SUCCESS' })
}).catch(e => {
store.dispatch({ type: 'PLUS_FAILED' })
})
return { type: 'PLUS_START' }
}
...
异步中间件
redux-thunk
上面是异步的基本用法,但是我们需要在异步请求的前后多次去调用 dispatch,这显得并不是那么方便。如果有一个办法可以直接在 dispatch 里面放一个异步操作的函数就会方便很多了,不过 dispatch 却只能将形如{ type: 'PLUS_SUCCESS' }的对象作为参数传进来。
想要赋予 dispatch 能够接收一个函数作为参数的能力那么就需要用到 redux-thunk 中间件了。
引入方法如下所示:
import { applyMiddleware, createStore } from 'redux'
import thunk from 'redux-thunk'
const store = createStore(
reducers,
applyMiddleware(thunk)
)
使用方法:
function asyncFunction(param) {
return dispatch => {
stop(1000, param).then(res => {
dispatch({ type: 'PLUS_SUCCESS' })
})
}
}
// 使用方法一
store.dispatch(asyncFunction(param))
// 使用方法二
store.dispatch(asyncFunction(param)).then(() =>
console.log(store.getState())
)
redux-promise
这个中间件可以让 dispatch 具备接收 promise 对象的能力。
引入方法如下所示:
import { createStore, applyMiddleware } from 'redux'
import promiseMiddleware from 'redux-promise'
import reducer from './reducers'
const store = createStore(
reducer,
applyMiddleware(promiseMiddleware)
)
使用方法:
// 如果 reject 将不会 dispatch action
function plusPromise() {
return new Promise((resolve, reject) => {
stop(1000, true).then(() => {
resolve({ type: 'PLUS_SUCCESS' })
})
})
}
store.dispatch(plusPromise())
Hooks
hook 版本简化了在组件中使用 redux,mapStateToProps、mapDispatchToProps、connect 还有中间件都不需要了。
子组件中调用 action:
- useSelector 替代 mapStateToProps
- useDispatch 替代 mapDispatchToProps,且可以不使用中间件直接进行异步操作
引入方法:
import { useSelector, useDispatch } from 'react-redux'
直接在函数组件中就可以使用,使用方法:
const count = useSelector(state => {
console.log(state)
return state.count
})
function plus() {
dispatch({ type: 'PLUS_SUCCESS' })
}
完整 Demo
pure-redux
reducer1.js、 reducer2.js 和 reducer.js 同 combineReducers 的例子。
index.js 如下:
import React from 'react'
import ReactDOM from 'react-dom'
import { bindActionCreators, createStore } from 'redux'
import reducers from './reducers'
function click() {
const store = createStore(reducers)
function action1() {
return { type: 'ACTION1', newState: Math.random() }
}
function action3() {
return { type: 'ACTION3', newState: Math.random() }
}
store.subscribe(() => console.log(store.getState()))
// store.dispatch({ type: 'ACTION1', newState: Math.random() })
// store.dispatch({ type: 'ACTION3', newState: Math.random() })
const action1Dispatch = bindActionCreators(action1, store.dispatch)
action1Dispatch()
store.dispatch(action3())
}
function Button() {
return (
<button onClick={click}>click me</button>
)
}
ReactDOM.render(
<Button />,
document.getElementById('root')
)
Redux 与 React 组件结合使用
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import { bindActionCreators, createStore } from 'redux'
function reducers(state = { count: 0 }, action) {
switch (action.type) {
case 'PLUS_ONE':
return { count: state.count + 1 }
case 'MINUS_ONE':
return { count: state.count - 1 }
default:
return state
}
}
const store = createStore(reducers)
function plusOne() {
return { type: 'PLUS_ONE' }
}
function minusOne() {
return { type: 'MINUS_ONE' }
}
class MyComponent1 extends React.Component {
render() {
const { count } = this.props
const { minusOne, plusOne } = this.props.actions
return (
<div>
<button onClick={minusOne}>-</button>
{count}
<button onClick={plusOne}>+</button>
</div>
)
}
}
function mapStateToProps(state) {
return {
count: state.count,
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({ plusOne, minusOne }, dispatch)
}
}
const ConnectedMyComponent1 = connect(mapStateToProps, mapDispatchToProps)(MyComponent1)
class MyComponent1WithStore extends React.Component {
render() {
return (
<Provider store={store}>
<ConnectedMyComponent1 />
</Provider>
)
}
}
ReactDOM.render(
<MyComponent1WithStore />,
document.getElementById('root')
)
中间件
import React from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import { bindActionCreators, createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
const logger = createLogger();
const store = createStore(
reducers,
applyMiddleware(logger)
)
function reducers(state = { count: 0 }, action) {
switch (action.type) {
case 'PLUS_ONE':
return { count: state.count + 1 }
case 'MINUS_ONE':
return { count: state.count - 1 }
default:
return state
}
}
function plusOne() {
return { type: 'PLUS_ONE' }
}
function minusOne() {
return { type: 'MINUS_ONE' }
}
class MyComponent1 extends React.Component {
render() {
const { count } = this.props
const { minusOne, plusOne } = this.props.actions
return (
<div>
<button onClick={minusOne}>-</button>
{count}
<button onClick={plusOne}>+</button>
</div>
)
}
}
function mapStateToProps(state) {
return {
count: state.count,
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({ plusOne, minusOne }, dispatch)
}
}
const ConnectedMyComponent1 = connect(mapStateToProps, mapDispatchToProps)(MyComponent1)
class MyComponent1WithStore extends React.Component {
render() {
return (
<Provider store={store}>
<ConnectedMyComponent1 />
</Provider>
)
}
}
ReactDOM.render(
<MyComponent1WithStore />,
document.getElementById('root')
)
异步操作
import React from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import { bindActionCreators, createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
function stop(ms, isSuccess) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (isSuccess) {
resolve('success')
} else {
reject('failed')
}
}, ms)
})
}
const logger = createLogger();
const store = createStore(
reducers,
applyMiddleware(logger)
)
function reducers(state = { count: 0, state: 'init' }, action) {
switch (action.type) {
case 'PLUS_START':
return { count: state.count, state: 'pending' }
case 'PLUS_SUCCESS':
return { count: state.count + 2, state: 'plus success' }
case 'PLUS_FAILED':
return { count: state.count, state: 'failed' }
case 'MINUS_START':
return { count: state.count, state: 'pending' }
case 'MINUS_SUCCESS':
return { count: state.count - 2, state: 'minus success' }
case 'MINUS_FAILED':
return { count: state.count, state: 'failed' }
default:
return state
}
}
function plusFailed() {
stop(1000, false).then(res => {
store.dispatch({ type: 'PLUS_SUCCESS' })
}).catch(e => {
store.dispatch({ type: 'PLUS_FAILED' })
})
return { type: 'PLUS_START' }
}
function minusFailed() {
stop(1000, false).then(res => {
store.dispatch({ type: 'MINUS_SUCCESS' })
}).catch(e => {
store.dispatch({ type: 'MINUS_FAILED' })
})
return { type: 'MINUS_START' }
}
function plusSuccess() {
stop(1000, true).then(res => {
store.dispatch({ type: 'PLUS_SUCCESS' })
}).catch(e => {
store.dispatch({ type: 'PLUS_FAILED' })
})
return { type: 'PLUS_START' }
}
function minusSuccess() {
stop(1000, true).then(res => {
store.dispatch({ type: 'MINUS_SUCCESS' })
}).catch(e => {
store.dispatch({ type: 'MINUS_FAILED' })
})
return { type: 'MINUS_START' }
}
class MyComponent1 extends React.Component {
render() {
const { count, state } = this.props
const { minusSuccess, plusSuccess, minusFailed, plusFailed } = this.props.actions
return (
<div>
<button onClick={minusSuccess}> - 成功</button>
<button onClick={minusFailed}> - 失败</button>
{count}
<button onClick={plusSuccess}> + 成功</button>
<button onClick={plusFailed}> + 失败</button>
<p>
{state}
</p>
</div>
)
}
}
function mapStateToProps(state) {
return {
count: state.count,
state: state.state,
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({ plusSuccess, minusSuccess, plusFailed, minusFailed }, dispatch)
}
}
const ConnectedMyComponent1 = connect(mapStateToProps, mapDispatchToProps)(MyComponent1)
class MyComponent1WithStore extends React.Component {
render() {
return (
<Provider store={store}>
<ConnectedMyComponent1 />
</Provider>
)
}
}
ReactDOM.render(
<MyComponent1WithStore />,
document.getElementById('root')
)
redux-thunk
import React from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import { bindActionCreators, createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
import thunk from 'redux-thunk';
function stop(ms, isSuccess) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (isSuccess) {
resolve('success')
} else {
reject('failed')
}
}, ms)
})
}
const logger = createLogger();
const store = createStore(
reducers,
applyMiddleware(thunk, logger)
)
function reducers(state = { count: 0, state: 'init' }, action) {
switch (action.type) {
case 'PLUS_SUCCESS':
return { count: state.count + 2, state: 'plus success' }
case 'MINUS_SUCCESS':
return { count: state.count - 2, state: 'minus success' }
default:
return state
}
}
function plusAsync() {
return dispatch => {
stop(1000, true).then(res => {
dispatch({ type: 'PLUS_SUCCESS' })
})
}
}
function minusAsync() {
return dispatch => {
stop(1000, true).then(res => {
dispatch({ type: 'MINUS_SUCCESS' })
})
}
}
class MyComponent1 extends React.Component {
render() {
const { count, state } = this.props
const { plusAsync, minusAsync } = this.props.actions
return (
<div>
<button onClick={minusAsync}>-</button>
{count}
<button onClick={plusAsync}>+</button>
<p>
{state}
</p>
</div>
)
}
}
function mapStateToProps(state) {
return {
count: state.count,
state: state.state,
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({ plusAsync, minusAsync }, dispatch)
}
}
const ConnectedMyComponent1 = connect(mapStateToProps, mapDispatchToProps)(MyComponent1)
class MyComponent1WithStore extends React.Component {
render() {
return (
<Provider store={store}>
<ConnectedMyComponent1 />
</Provider>
)
}
}
ReactDOM.render(
<MyComponent1WithStore />,
document.getElementById('root')
)
redux-promise
import React from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import { bindActionCreators, createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
import promiseMiddleware from 'redux-promise'
function stop(ms, isSuccess) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (isSuccess) {
resolve('success')
} else {
reject('failed')
}
}, ms)
})
}
const logger = createLogger();
const store = createStore(
reducers,
applyMiddleware(promiseMiddleware, logger)
)
function reducers(state = { count: 0, state: 'init' }, action) {
switch (action.type) {
case 'PLUS_SUCCESS':
return { count: state.count + 2, state: 'plus success' }
case 'MINUS_SUCCESS':
return { count: state.count - 2, state: 'minus success' }
default:
return state
}
}
// 如果 reject 将不会 dispatch action
function plusPromise() {
return new Promise((resolve, reject) => {
stop(1000, true).then(() => {
resolve({ type: 'PLUS_SUCCESS' })
})
})
}
function minusPromise() {
return new Promise((resolve, reject) => {
stop(1000, true).then(() => {
resolve({ type: 'MINUS_SUCCESS' })
})
})
}
class MyComponent1 extends React.Component {
render() {
const { count, state } = this.props
const { plusPromise, minusPromise } = this.props.actions
return (
<div>
<button onClick={minusPromise}>-</button>
{count}
<button onClick={plusPromise}>+</button>
<p>
{state}
</p>
</div>
)
}
}
function mapStateToProps(state) {
return {
count: state.count,
state: state.state,
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({ plusPromise, minusPromise }, dispatch)
}
}
const ConnectedMyComponent1 = connect(mapStateToProps, mapDispatchToProps)(MyComponent1)
class MyComponent1WithStore extends React.Component {
render() {
return (
<Provider store={store}>
<ConnectedMyComponent1 />
</Provider>
)
}
}
ReactDOM.render(
<MyComponent1WithStore />,
document.getElementById('root')
)
Hooks
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
import { useSelector, useDispatch } from 'react-redux'
function stop(ms, isSuccess) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (isSuccess) {
resolve('success')
} else {
reject('failed')
}
}, ms)
})
}
const logger = createLogger();
const store = createStore(
reducers,
applyMiddleware(logger)
)
function reducers(state = { count: 0, state: 'init' }, action) {
switch (action.type) {
case 'PLUS_SUCCESS':
return { count: state.count + 2, state: 'plus success' }
case 'MINUS_SUCCESS':
return { count: state.count - 2, state: 'minus success' }
default:
return state
}
}
function MyComponent1() {
const { count, state } = useSelector(state => {
console.log(state)
return state
});
const dispatch = useDispatch();
function plus() {
stop(1000, true).then(() => {
dispatch({ type: 'PLUS_SUCCESS' })
})
}
function minus() {
dispatch({ type: 'MINUS_SUCCESS' })
}
return (
<div>
<button onClick={minus}>-</button>
{count}
<button onClick={plus}>+</button>
<p>
{state}
</p>
</div>
)
}
ReactDOM.render(
<Provider store={store}>
<MyComponent1 />
</Provider>,
document.getElementById('root')
)