reactjsreduxredux-thunkredux-toolkitdispatch-async

How to resolve async chaining using thunk in redux


I am trying to dispatch two posts at the same time using thunk in Redux but when I try to dispatch the second post I am missing the user id from the previous post. There might be a process on how to async chain these requests but I haven't been able find a good solution. It seems that userSelector supposed to keep its state. I have tried this link too: https://blog.jscrambler.com/async-dispatch-chaining-with-redux-thunk. Promise.all is not recommended but I also tried that as well as dispatch(…).then(() => dipatch()). Any feedback is great!!

import { configureStore, createAsyncThunk, createSlice, unwrapResult } from '@reduxjs/toolkit';

export const postInfo = createAsyncThunk(
  'info/postInfo'
  async (infoObject: InfoRequest) => {
    const response = await axios.post(`${url}/info`, infoObject);
    return response.data;
  }
);

export const postSales = createAsyncThunk(
  'sales/postSales'
  async (salesObject: SalesRequest) => {
    const response = await axios.post(`${url}/sales`, salesObject);
    return response.data;
  }
);
...
const postInfoSlice = createSlice<PostState, SliceCaseReducers<PostState>, string>({
  name: 'postInfo',
  sales: 'postSales',
  initialState: {
    request: { status: 'idle', error: '' },
  },
  reducers: {},
  extraReducers: (builder) => {
      builder.addCase(postInfo.fulfilled, (state, action) => {
        state.request.status = 'succeeded';
        state.model = action.payload;
      }
      builder.addCase(postInfo.rejected, (state, action) => {
        state.request.status = 'failed';
        state.request.error = action.error.message as string;
      })
      builder.addCase(postSales.fulfilled, (state, action) => {
        state.request.status = 'succeeded';
        state.model = action.payload;
      }
      builder.addCase(postSales.rejected, (state, action) => {
        state.request.status = 'failed';
        state.request.error = action.error.message as string;
      })
   },
})

...

const store = configureStore({
    reducer:
        postInfoSlice.reducer
});
export type RootState = ReturnType<typeof store.getState>;
constant user = useSelector((state: RootState) => state.postState.user);

const sendInfoRequest = async () => {
  try {
    const infoObjRequest: InfoRequest = {
      firstName: 'John',
      lastName: 'Smith'
    };
    await dispatch(postInfo(infoObjRequest)).unwrap();
  } catch (err) {    
       console.log('rejected for post /info', err);
  }
};

const sendSalesRequest = async () => {
  try {
    const salesObjRequest: SalesRequest = {
      firstName: 'John',
      lastName: 'Smith',
      userId: user?.id
    };
    await dispatch(postSales(salesObjRequest)).unwrap();
  } catch (err) {    
       console.log('rejected for post /sales', err);
  }
};

// Here is where I am dispatching both post where sendSalesRequest can't get the user id from previous postInfo. 

sendInfoRequest();
sendSalesRequest();


Solution

  • Why not pass the ID as the variable in your request?

    //is user null or not initialized?
    sendInfoRequest(user?.id);
    sendSalesRequest(user?.id);
    

    There's nothing wrong with sending something explicitly--especially in a asynchronous context (it is also easier to test).

    I think the bigger issue is your state might not be what you expect (like null). In my experience it makes the most sense to keep things as simple an explicit as possible.

    In response to the comment I have some trouble understanding what you're asking, but if I understand the logic you basically want to do:

     -> Send request -> get ID -> use ID
    

    Fundamentally that can't be done up front without knowing the id. What you probably want is something like:

    -> Send request (wait)
      -> with data do {
            action1, action2, etc...
      }
    

    There's not enough code to give you any real information beyond that. If the user id doesn't exist in your state you need to request and use it. In redux that usually looks something like

    //And please forgive me, there are A LOT of different ways to write this
    ...
    const doAfterUserIdExists = (userId) => {
      dispatch(a)
      dispatch(b)
      ...
      dispatch(x)
    }
    dispatch( initialAction(doAfterUserIdExists) )
    
    //--in the backend
    export const initialAction = (callback) => {
       return dispatch => {
           //do some business logic
           ...
           const user = new User()//ID is created
           if(callback) {
             callback(user)
           }
    
           dispatch({
             type: CASCADE_USER_FUNCTION,
             user: user,
           })
       }
    }
    

    It isn't that different from what you're doing, except it has a linear flow. Promise.all() also isn't viable because it will run all your events at the same time (pointless, you need an ID first).

    This isn't a perfect solution, but it gives you an idea of how to control the flow of data. You could also investigate Sagas or other patterns to make "thunks" work. Alternatively you could flip it so that the "sub logic" like posting info and sales requests happens in the back-end if they're part of a transaction.

    It isn't magic, you need to find a solution that works for you. I tend to lean on callbacks because they are a linear flow, but there are many different patterns. I find this one the easiest to read.