createAsyncThunk - Redux Toolkit

createAsyncThunk - Redux Toolkit

Learn how to use the createAsyncThunk API to perform asynchronous tasks in Redux apps and handle common errors.

By using createAsyncThunk, the code in the actions file becomes much shorter:

cart-actions.js:

import { createAsyncThunk } from '@reduxjs/toolkit';

export const fetchCartData = createAsyncThunk(
  'cart/fetchData',
  async () => {
    const response = await fetch(`${firebaseUrl}/cart.json`);
    if (!response.ok) throw new Error();
    const data = await response.json();
    return {
      items: data?.items || [],
      totalQuantity: data?.totalQuantity || 0
    };
  }
);

export const sendCartData = createAsyncThunk(
  'cart/sendData',
  async (cart) => {
    const config = {
      method: 'PUT',
      body: JSON.stringify({ 
        items: cart.items, 
        totalQuantity: cart.totalQuantity 
      })
    };
    const response = await fetch(`${firebaseUrl}/cart.json`, config);
    if (!response.ok) throw new Error();
  }
);

1- The first argument passed to createAsyncThunk is a string identifier which will be used for the automatically generated actions (see 3).

2- The second argument is basically the same code like in the corresponding functions from Max' original code. But it can be simplified a lot, since we don't have to handle errors ourselves, and we don't need to dispatch any actions.

3- React Toolkit automatically generates and dispatches actions initially and when the Promise resolves (which is returned from any async function in JS).

4- The names of these actions are generated from the first argument (see 1) and can be inspected in the Redux DevTools (for example cart/fetchData/pending, cart/fetchData/fulfilled and cart/fetchData/rejected).

Please note:

In my example above the return value in fetchCartData also takes into account the initial state where a cart might not yet exist in firebase (which is not handled in the original course code). But this modification is not related to createAsyncThunk, of course.

In the createSlice methods we can then use the automatically created actions in the way shown below.

Important: The related methods have to be added in an extraReducers object, and not in the normal reducers object, since - as we know - for the latter a set of new actions would be created under the hood - and this work has already be done by createAsyncThunk above

cart-slice.js:

import { createSlice } from '@reduxjs/toolkit';
import { fetchCartData } from './cart-actions';

const cartSlice = createSlice({
  name: 'cart',
  initialState: { /* see original code */ },
  reducers: {
    /* remove replaceCart action of original code */
    addItemToCart(state, action) { /* see original code */ },
    removeItemFromCart(state, action) { /* see original code */ }
  },
  extraReducers: {
    [fetchCartData.fulfilled]: (state, action) => {
      state.totalQuantity = action.payload.totalQuantity;
      state.items = action.payload.items;
    }
  }
});

export const cartActions = cartSlice.actions;
export default cartSlice;

ui-slice.js:

import { createSlice } from '@reduxjs/toolkit';
import { fetchCartData, sendCartData } from './cart-actions';

const uiSlice = createSlice({
  name: 'ui',
  initialState: { /* see original code */ },
  reducers: {
    toggle(state) { /* see original code */ }
    /* remove showNotification action of original code */
  },   
  extraReducers: {
    [fetchCartData.rejected]: (state, action) => {
      state.notification = {
        status: 'error', 
        title: 'Error!', 
        message: action.error.message || 'Fetch failed'
      };
    },
    [sendCartData.pending]: (state) => {
      state.notification = {
        status: '', 
        title: 'Pending ...', 
        message: 'Sending Books ...'
      };
    },
    [sendCartData.fulfilled]: (state) => {
      state.notification = {
        status: 'success', 
        title: 'Success!', 
        message: 'Cart data sent successfully!'
      };
    },
    [sendCartData.rejected]: (state, action) => {
      state.notification = {
        status: 'error', 
        title: 'Error!', 
        message: 'Failed to send cart data!'
      };
    }
  }
});

export const uiActions = uiSlice.actions;
export default uiSlice;

Additional info:

1- As far as I can see, instead of using the action creator names in the extraReducers, e.g. [fetchCartData.fulfilled], it would theoretically also be possible to use the action types (like 'cart/fetchData/fulfilled'). But it's very important not to mix up these both notations.

2- It would be no problem to use the automatically generated actions, e.g. [fetchCartData.fulfilled] in the extraReducers of multiple slices, like here in the cart slice and the ui slice (if needed).

3- For the extraReducers there is also an alternative syntax - which is a little more verbose, but recommended if we want to use TypeScript:

extraReducers: (builder) => {
  builder
  .addCase(
    sendCartData.pending,
   (state, action) => {
     state.notification = {
       status: '', 
       title: 'Pending ...', 
       message: 'Sending Books ...'
     };
    }
  )
  .addCase(...)
  .addCase(...)
  ...
}

https___cdn.qiita.com_assets_public_article-ogp-background-9f5428127621718a910c8b63951390ad (1).png


Screenshot (456).png

Screenshot (458).png