Search code examples
reduxpromisees6-promiseredux-thunkredux-toolkit

A clean way for an action to fire multiple asynchronous actions with createAsyncThunk


We're delaying the rendering of our React-Redux web app until several asynchronous app initialization tasks in the Redux store have been completed.

Here's the code that sets up the store and then fires off the initialization action:

export const setupStoreAsync = () => {
    return new Promise((resolve, reject) => {
        const store = setupStore()
        store
            .dispatch(fetchAppInitialization())
            .then(unwrapResult)
            .then(_ => resolve(store))
            .catch(e => reject(e.message))
    })
}

The promise rejection is very important since it's used to render an error message for the user in case the app cannot be properly set up. This code is very nice to read and works wonderfully.

The issue is with the action creator:

export const fetchAppInitialization = createAsyncThunk(
    'app/initialization',
    (_, thunkApi) =>
        new Promise((resolve, reject) =>
            Promise.all([thunkApi.dispatch(fetchVersionInfo())]).then(results => {
                results.map(result => result.action.error && reject(result.error))
            })
        )
)

This code works beautifully. If any of these actions fail, the promise is rejected and the user sees an error message. But it's ugly - It's not as pretty as our normal action creators:

export const fetchVersionInfo = createAction('system/versionInfo', _ => ({
    payload: {
        request: { url: `/system/versionInfo` },
    },
}))

We will at some point fire more than one fetch request in fetchAppInitialization, so the Promise.all function is definitely required. We'd love to be able to use Redux-Toolkit's createAction syntax to fire multiple promisified actions in order to shorten this action creator, but I have no idea if that's even possible.

Note: I'm using redux-requests to handle my axios requests.

Is createAsyncThunk even required?


Solution

  • Since I wasn't using the fetchAppInitialization action for anything but this single use case, I've simply removed it and moved the logic straight into the setupStoreAsync function. This is a bit more compact. It's not optimal, since the results.map logic is still included, but at least we don't use createAsyncThunk any more.

    export const setupStoreAsync = () => {
        return new Promise((resolve, reject) => {
            const store = setupStore()
    
            new Promise((resolve, reject) =>
                Promise.all([store.dispatch(fetchVersionInfo())]).then(results => {
                    results.map(result => result.action.error && reject(result.error))
                    resolve()
                })
            )
                .then(_ => resolve(store))
                .catch(e => reject(e.message))
        })
    }
    

    Update: I was able to make the code even prettier by using async/await.

    export const setupStoreAsync = async () => {
        const store = setupStore()
    
        const results = await Promise.all([store.dispatch(fetchVersionInfo())])
        results.forEach(result => {
            if (result.action.error) throw result.error
        })
        return store
    }