Search code examples
reactjstypescriptreduxreact-reduxredux-toolkit

Reducers vs ExtraReducers as a way to handle thunks


I'm currently converting an old redux-codebase to use Redux-Toolkit (RTK), and I can't seem to understand the difference between these approaches. Consider this slice:

interface MyState {
  someItems: string[];
}

const myInitialState: MyState = {
  someItems: []
};

const mySlice = createSlice({
  name: 'mySlice',
  initialState: myInitialState,
  reducers: {
    setItems(state, action: PayloadAction<string[]>) {
      state.someItems = action.payload;
    }
  },
  extraReducers: builder => {
    builder.addCase(fetchItems.fulfilled, (state, action) => {
      state.someItems = action.payload;
    });
  }
});

My traditional way to use redux has been to call the reducer directly from a Thunk, but I see that the way defined in extraReducers is also possible.

const fetchItems = createAppAsyncThunk(
  'mySlice/fetchItems',
  async (_, thunkAPI) => {
    try {
      //Get data from backend
      const response = await getItems();

      //Option 1
      thunkAPI.dispatch(mySlice.actions.setItems(response));
      //Option 2
      return response;
    } catch (e) {
      return thunkAPI.rejectWithValue('failed');
    }
  }
);

What is the difference between these approaches? Are any of them preferred over the other? Lastly when doing changes I often have to reload the data for latest updates. This time I believe I have to use dispatch inside the Thunk. Or are there alternatives?

const editItem = createAppAsyncThunk<void, string>(
  'mySlice/fetchItems',
  async (changedValue, thunkAPI) => {
    try {
      //Send data to backend
      await sendSomethingToBackend(changedValue);

      thunkAPI.dispatch(fetchItems());
    } catch (e) {
      return thunkAPI.rejectWithValue('failed');
    }
  }
);

Solution

  • Given:

    const fetchItems = createAppAsyncThunk(
      'mySlice/fetchItems',
      async (_, thunkAPI) => {
        try {
          //Get data from backend
          const response = await getItems();
    
          // Option 1
          thunkAPI.dispatch(mySlice.actions.setItems(response));
          // Option 2
          return response;
        } catch (e) {
          thunkAPI.rejectWithValue('failed');
        }
      }
    );
    

    What is the difference between these approaches?

    In the grand scheme of things there's not much difference between the two with regards to the single mySlice state slice. The main difference comes down to what, or where, can handle the "resolved" thunks.

    • Dispatch an explicit action: thunkAPI.dispatch(mySlice.actions.setItems(response));

      • Only the mySlice setItems reducer function can respond and handle this action.
    • Dispatch an implicit resolved (fulfilled) action: return response;

      • Any Redux-Toolkit (RTK) slice's extraReducers functions can respond and handle the fetchItems.fulfilled action.

    Are any of them preferred over the other?

    This is subjective, but using the Thunk's .pending, .fulfilled, and .rejected actions is likely the generally accepted preferred method as it provides greater flexibility. One of the paramount goals of RTK was to cut down on the amount of code you needed to write yourself. Using the automatically generated Thunk actions is part of this goal.

    const editItem = createAppAsyncThunk<void, string>(
      'mySlice/fetchItems',
      async (changedValue, thunkAPI) => {
        try {
          //Send data to backend
          await sendSomethingToBackend(changedValue);
    
          thunkAPI.dispatch(fetchItems());
        } catch (e) {
          thunkAPI.rejectWithValue('failed');
        }
      }
    );
    

    Lastly when doing changes I often have to reload the data for latest updates. This time I believe I HAVE to use dispatch inside the thunk.

    Yes, here you absolutely would need to dispatch the fetchItems action to refetch data that sendSomethingToBackend may've updated in the backend.

    Or are there alternatives?

    If you are fetching and updating data then an alternative for you may be to use Redux-Toolkit Query (RTK Query). You'd create an API slice that manages queries and mutations. Convert the fetchItems Thunk to a query, and the editItem Thunk to a mutation, and using cache tags the mutations can invalidate queried data and trigger queries to re-fetch automatically. In other words, further cutting down of code you write.