2023年9月26日
By: Chase

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的数据.

图-2

不用thunk

不用thunk1

可以dispatchA的时候携带stateB的数据作为参数. 如下(绿色部分): 图-5

示例代码:

// 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的初衷.

图-6

用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, 让你猜猜结果:

state0state1都会一样吗?

// 不用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, 能解决闭包带来的问题.

thunkDispatchAstate1是最新的state. 而dispatchANew不是.

优化多个组件didMount阶段调用了同一个dispatch

举例一个业务场景:

图-7

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
    }
})
Tags: 前端 redux-thunk React