redux-thunk解决react中的异步state数据流
前言
本文适合已有一定基础的react
开发者阅读, 主要结合几个实际业务场景解释为什么要用redux-thunk
.
附上官网
介绍
网上多数对redux-thunk
的介绍大多聚焦于将action(dispatch)
与API
请求封装在一起. 可以将异步接口请求和更新redux从UI组件中剥离出来封装复用, 同时让UI组件
更加纯净.
比如以下来自官网的例子
// thunk封装
export const fetchTodoById = todoId => async dispatch => {
const response = await client.get(`/fakeApi/todo/${todoId}`)
dispatch({
type: "UPDATE_RES",
param: response.todos
})
}
// UI组件内调用
dispatch(fetchTodoById(id))
但是这对于已经拥有hooks
的react使用者来说, 不那么必要.
如下封装可以起到同样的作用, 而且更"react"
// 请求hooks封装
export const useFetch = () => {
const fetchTodoById = async (todoId) => {
const response = await client.get(`/fakeApi/todo/${todoId}`)
dispatch({
type: "UPDATE_RES",
param: response.todos
})
}
return { fetchTodoById }
}
// UI组件内调用
const { fetchTodoById } = useFetch()
fetchTodoById(todoId)
直至我遇到了以下场景, 不得已又把thunk
翻了出来.
业务场景
dispatch需要依托特定state值
举例一个简单的代码框架设计图, dispatchA
的时候需要拿stateB
的数据.
不用thunk
不用thunk1
可以dispatchA的时候携带stateB的数据作为参数. 如下(绿色部分):
示例代码:
// componentA
export const ComponentA = () => {
const dispatch = useDispatch()
const { stateA } = useSelector(state => state.reducerA)
const { stateB } = useSelector(state => state.reducerB)
const dispatchANew = () => {
// type:A 代表上面图例中的dispatchA
dispatch({
type: A,
param: stateB
})
}
}
不用thunk2
合并stateA
, stateB
到一个ReducerAB
, 在Reducer内部拿stateB
, 但是并不符合对Redux拆分Reducer的初衷.
用thunk
代码示例:
const thunkDispatchA = (param) => {
return (dispatch, getState) => {
const state = getState()
// type:A 代表上面图例中的dispatchA
dispatch({
type: A,
param: state.stateB
})
}
}
// ComponentA内部
dispatch(thunkDispatchA())
thunkDispatchA
这里的getState()
能从reducer里拿到state.
上面的不用不用thunk1中, ComponentA
直接订阅stateB
同样也能拿到. 为什么要花这么个功夫绕路引入thunk???
下面我们分别在不用thunk1, 和用thunk的示例代码中加上一些console.log, 让你猜猜结果:
state0
和state1
都会一样吗?
// 不用thunk1中代码
const dispatchANew = () => {
console.log('state0', state)
dispatch({
type: A,
param: stateB
})
console.log('state1', state)
}
// 用thunk代码
const thunkDispatchA = (param) => {
return (dispatch, getState) => {
const state = getState()
console.log('state0', state)
dispatch({
type: A,
param: state.stateB
})
const stateNew = getState()
console.log('state1', stateNew)
}
}
是的, 不同就在于getState()
是一个function, 再次调用获取state, 能解决闭包带来的问题.
thunkDispatchA
的state1
是最新的state. 而dispatchANew
不是.
优化多个组件didMount阶段调用了同一个dispatch
举例一个业务场景:
navbar
作为一个全局组件, 内部封装了获取加载userName
, 而ComponentA1/A2
在调接口时需要携带userName
.
当ComponentA1/A2
, navbar
组件同时作为ComponentParent
下子组件init加载时, ComponentA1/A2
调用接口时, 未必能拿到userName
, 所以不用thunk
可以用以下两个方案
// 方案1
useEffect(() => {
initComponentA()
}, [])
const initComponentA = async () => {
const userName = await client.get('/userName')
const stateA = await clinet.get(`/stateA/${userName}`)
dispatch({ type: A })
}
// 方案2
const { userName } = useSelector(state => state.global)
useEffect(() => {
if (userName) {
initComponentA(userName)
}
}, [userName])
方案1, 如果有多个类似ComponentA
的组件, 无疑会多很多次不必要的/userName
接口请求.
方案2, 如果/stateA
请求需要很多类似userName
的依赖, 那么我们的useEffect()
的[]
需要把每一个依赖都加上并加以判断. []
内的依赖项越多, 代码越难以维护.
如果用thunk
, 可以:
const thunkFetchAndInitComponentA = (param) => {
return async (dispatch, getState) => {
let { userName } = getState().global
if (!userName) {
userName = await client.get('/userName')
dispatch({
type: "UPDATE_USER_NAME",
param: userName
})
}
const stateA = await clinet.get(`/stateA/${userName}`)
dispatch({ type: A })
}
}
当然, 如果client.get('/userName')
是个比较耗时的请求, 还是无法避免并发请求同个接口多次, 再进一步则需要在API请求层面
做优化, 比如Axios
可以通过cancelToken
或者controller.abort()
取消重复发出的请求.
引入
新版redux, store.js
配置, 据官网介绍, 已经内置了thunk组件. 不再赘述.
import { configureStore } from '@reduxjs/toolkit'
import storeAReducer from './storeAReducer'
import storeBReducer from './storeBReducer'
export default configureStore({
reducer: {
storeA: storeAReducer,
storeB: storeBReducer
}
})