Redux Toolkit: Async Thunk not wiring up to my reducers properly

Viewed 115

I am trying to figure out why my async thunk call is not working as expected. I am trying to pull out an array of JSON objects from my DB when the front page of my website loads. Right now the api is hit and the data gets pulled out but something falls apart between the action creator and the slice. I am trying to console.log action and state but nothing comes up either.

I am just learning redux-toolkit so I am pretty new to this.

Here is my slice:

import { createSlice } from '@reduxjs/toolkit'

const prizesSlice = createSlice({
  name: 'prizes',
  initialState: {
    prizesArray: [],
  },
  reducers: {
    getPrizes(state, action) {
      console.log('hello')
      state.prizes = action.payload
    },
  },
})

export const prizesSliceActions = prizesSlice.actions
export default prizesSlice

Here is the action-creator:

import { prizesSliceActions } from './prizes-slice'
import { createAsyncThunk } from '@reduxjs/toolkit'
import * as api from '../apiClient'

// get data
export const fetchPrizeData = createAsyncThunk(
  'prizesArray/getPrizes',

  async () => {
    const data = await api.getAllPrizes()
    console.log(data)
    prizesSliceActions.getPrizes(data)
  }
)

Here is how it is called in teh component:

import { fetchPrizeData } from '../store/prizes-actions'
[...]
  useEffect(() => {
    dispatch(fetchPrizeData())
  }, [dispatch])

Here is my store:

import { configureStore } from '@reduxjs/toolkit'

import prizesSlice from './prizes-slice'

const store = configureStore({
  reducer: { prizes: prizesSlice.reducer },
})

export default store
1 Answers

Ok, let me try to clear it up:

The main problem

The main problem is that the action object, created by the "case reducer" function getPrizes, never gets dispatched to the actual redux store.

Sounds overwhelming? Let me explain in detail.

The pricesSlice

There is one problem with how you set up the slice.

You set your initial state to an object that has one property named prizesArray, which is for now initialized with an empty array. All is good so far.

Now you declared a reducer named getPrizes, more specifically a "case reducer", which combines a redux action and reducer in one go.

Also, this is fine, however, the naming is a bit ambiguous to me. This is a function that "changes" the state, so it doesn't really get anything, right? What it really does is setting the prizes. So a more proper name would probably be setPrizes.

Okay, that aside, now there is a real problem. The case reducer looks like this:

reducers: {
  getPrizes(state, action) {
    console.log('hello')
    state.prizes = action.payload
  },
},

You access state.prizes, but this field doesn't exist in the state! Remember in initialState the name was set to prizesArray. So properly the function would look like this:

reducers: {
  setPrizes(state, action) {
    state.prizesArray = action.payload
  },
},

Or rename the field in the initial state, it's up to you.

Now that we fixed the pricesSlice it's time to fix the other parts.

The async action thunk

You create an asyncActionThunk, which is good! However, you then call prizesSlice.actions.getPrizes(data) but you never dispatch the action object this function created.

I converted your code for a second to typescript, so we actually can see the return type of that operation:

The return type

The return type of that function call is just an object. The function call did nothing else than just create a "redux action" object for you (or in other words, your reducer function never got called yet). It's now up to you to pass that action object to redux to "apply the changes". We usually dispatch action objects to redux, so a potential fix could look like this:

export const fetchPrizeData = createAsyncThunk(
  "prizesArray/getPrizes",

  //  - We are getting the thunkApi as second arg.
  // On it, we have the dispatch method available to us
  async (_, thunkApi) => {
    const data = await new Promise((r) => r(["1", "2"]));
    // now we dispatch the action object to the redux store
    // the state gets updated, yei!
    thunkApi.dispatch(prizesSliceActions.setPrizes(data));
  }
);

This already fixes the issue, as we now dispatch the object to redux. You can read more about the thunkAPI argument in createAsyncThunk here in the documentation.

HOWEVER, this is not the best approach when using createAsyncThunk. We actually can leverage it completely. I'd just follow the documentation:

export const fetchPrizeData = createAsyncThunk("prizes/getPrizes", async () => {
  const data = await api.getAllPrizes()
  console.log(data)
  // We just return our data here
  return data;
});

const prizesSlice = createSlice({
  name: "prizes",
  initialState: {
    prizesArray: []
  },
  reducers: {},
  extraReducers: (builder) => {
    // we setup an extra reducer that will update the state
    // when the async thunk resolved
    builder.addCase(fetchPrizeData.fulfilled, (state, action) => {
      state.prizesArray = action.payload;
    });
  }
});

This approach is using createAsyncThunk more properly, and as such we also get the benefit of clearer actions being dispatched on the store. For example if we inspect the dispatched actions with redux dev tools we see that the dispatched action prizes/getPrizes/fullfilled is the action that leads to the state being updated:

createAsyncThunk actions in redux debug tool

With the other approach, we would have dispatched three actions on the redux store, and it's better to keep the actions dispatched as meaningful as possible!

Hope this clears it up, if there are questions left, just let me know!

Related