'Calling other actions from createAsyncThunk

Usually in a thunk you'd wind up calling other actions:

const startRecipe = {type: "startRecipe"}

const reducer = (state, action) => {
  if (action.type === "startRecipe") { 
    state.mode = AppMode.CookRecipe
  }
}

const getRecipeFromUrl = () => async dispatch => {
  const res = await Parser.getRecipeFromUrl(url)
  dispatch(startRecipe)
}

With createAsyncThunk in redux toolkit, this isn't so straightforward. Indeed you can mutate the state from your resulting action in extraReducers:

export const getRecipeFromUrl = createAsyncThunk('getRecipeFromUrl',
  async (url: string): Promise<RecipeJSON> => await Parser.getRecipeFromUrl(url)
)

const appStateSlice = createSlice({
  name: 'app',
  initialState: initialAppState,
  reducers: {},
  extraReducers: ({ addCase }) => {
    addCase(getRecipeFromUrl.fulfilled, (state) => {
      state.mode = AppMode.CookRecipe
    })
  }
})

But I also want to have non-async ways to start the recipe, which would entail a reducer in the slice:

  reducers: {
    startRecipe(state): state.mode = AppState.CookRecipe
  },

To avoid writing the same code in two places I would love to be able to call the simple reducer function from the thunk handler. I tried simply startRecipe(state) and startRecipe (which had been destructured for ducks exporting so I’m fairly sure I was referring to the correct function) from the extraReducers case but it doesn't work.

My current solution is to define _startRecipe outside of the slice and just refer to that function in both cases

  reducers: { startRecipe: _startRecipe },
  extraReducers: builder => {
    builder.addCase(getRecipeFromUrl.fulfilled, _startRecipe)
  }

Is there a "better" way where you can define the simple action in your slice.reducers and refer to it from the thunk handler in extraReducers?



Solution 1:[1]

The second argument of the payloadCreator is thunkAPI (doc) from where you could dispatch the cookRecipe action.

interface ThunkApiConfig {
  dispatch: AppDispatch,
  state: IRootState,
}

export const getRecipeFromUrl = createAsyncThunk('getRecipeFromUrl',
  async (url: string, thunkAPI: ThunkApiConfig): Promise<RecipeJSON> => {
    await Parser.getRecipeFromUrl(url)
    return thunkAPI.dispatch(cookRecipeActionCreator())
  }
)

Solution 2:[2]

The idea of "calling a reducer" is the wrong approach, conceptually. Part of the design of Redux is that the only way to trigger a state update is by dispatching an action.

If you were writing the reducer using a switch statement, you could have multiple action types as cases that all are handled by the same block:

switch(action.type) {
  case TypeA:
  case TypeB: {
    // common logic for A and B
  }
  case C: // logic for C
}

When using createSlice, you can mimic this pattern by defining a "case reducer" function outside of the call to createSlice, and pass it for each case you want to handle:

const caseReducerAB = (state) => {
  // update logic here
}

const slice = createSlice({
  name: "mySlice",
  initialState,
  reducers: {
    typeA: caseReducerAB,
    typeB: caseReducerAB,
  }
  extraReducers: builder => {
    builder.addCase(someAction, caseReducerAB)
  }
})

That sounds like what you described as your "current solution", so yes, that's what I would suggest.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 markerikson