Chinwike Maduabuchi Frontend developer passionate about software engineering.

Using Redux Toolkit’s createAsyncThunk

4 min read 1296

Using Redux Toolkit's createAsyncThunk

While a Redux store possesses great state management features, it has no clue how to deal with asynchronous logic. Redux eschews handling asynchronous logic simply because it doesn’t know what you want to do with the data you fetched, let alone if it’s ever fetched — hello, errors. 🙂

Middleware has since been used in Redux applications to perform asynchronous tasks, with Redux Thunk’s middleware being the most popular package. A middleware is designed to enable developers to write logic that has side effects — which refers to any external interaction outside an existing client application, like fetching data from an API.

With Redux Toolkit, Redux Thunk is included by default, allowing createAsyncThunk to perform delayed, asynchronous logic before sending the processed result to the reducers.

In this article, you’ll learn how to use the createAsyncThunk API to perform asynchronous tasks in Redux apps.

Prerequisites

You’ll need to have some knowledge about Redux to understand Redux Toolkit. However, you can reference this post to learn how to create Redux apps with Redux Toolkit.

Understanding the function parameters

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

const initialState = {
  entities: [],
  loading: false,
}

const getPosts = createAsyncThunk(
  //action type string
  'posts/getPosts',
  // callback function
  async (thunkAPI) => {
    const res = await fetch('https://jsonplaceholder.typicode.com/posts').then(
    (data) => data.json()
  )
  return res
})


export const postSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {},
  extraReducers: {},
})

export const postReducer = postSlice.reducer

The file above is a Redux slice in a React app. A slice is a function that contains your store and reducer functions used to modify store data. The createSlice API is set to be the norm for writing Redux logic.

Within createSlice, synchronous requests made to the store are handled in the reducers object while extraReducers handles asynchronous requests, which is our main focus.

Asynchronous requests created with createAsyncThunk accept three parameters: an action type string, a callback function (referred to as a payloadCreator), and an options object.

Taking the previous code block as a Redux store for a blog application, let’s examine getPosts:

const getPosts = createAsyncThunk(
  'posts/getPosts',
  async (thunkAPI) => {
    const res = await fetch('https://jsonplaceholder.typicode.com/posts').then(
    (data) => data.json()
  )
  return res
})

posts/getPosts is the action type string in this case. Whenever this function is dispatched from a component within our application, createAsyncThunk generates promise lifecycle action types using this string as a prefix:

We made a custom demo for .
No really. Click here to check it out.

  • pending: posts/getPosts/pending
  • fulfilled: posts/getPosts/fulfilled
  • rejected: posts/getPosts/rejected

On its initial call, createAsyncThunk dispatches the posts/getPosts/pending lifecycle action type. The payloadCreator then executes to return either a result or an error.

In the event of an error, posts/getPosts/rejected is dispatched and createAsyncThunk should either return a rejected promise containing an Error instance, a plain descriptive message, or a resolved promise with a RejectWithValue argument as returned by the thunkAPI.rejectWithValue function (more on thunkAPI and error handling momentarily).

If our data fetch is successful, the posts/getPosts/fulfilled action type gets dispatched.

The options parameter is an object containing different configurations for the createAsyncThunk API. View the list of available options.

The three lifecycle action types mentioned earlier can then be evaluated in extraReducers, where we make our desired changes to the store. In this case, let’s populate entities with some data and appropriately set the loading state in each action type:

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

const initialState = {
  entities: [],
  loading: false,
}

const getPosts = createAsyncThunk(
  'posts/getPosts',
  async (thunkAPI) => {
    const res = await fetch('https://jsonplaceholder.typicode.com/posts').then(
    (data) => data.json()
  )
  return res
})


export const postSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {},
  extraReducers: {
    [getPosts.pending]: (state) => {
      state.loading = true
    },
    [getPosts.fulfilled]: (state, { payload }) => {
      state.loading = false
      state.entities = payload
    },
    [getPosts.rejected]: (state) => {
      state.loading = false
    },
  },
})

export const postReducer = postSlice.reducer

If you’re new to Redux Toolkit, the state logic above might seem off to you. Redux Toolkit makes use of the Immer library, which allows developers to write mutable logic in reducer functions. Immer then converts your mutable logic to immutable logic under the hood.

Also, notice the function expression. For personal preference, I’ve used the map-object notation to handle the requests, mainly because this approach looks tidier.

The recommended way to handle requests is the builder callback notation because this approach has better TypeScript support (and thus IDE autocomplete even for JavaScript users).

N.B.: As your application grows, you will continue to make more asynchronous requests to your backend API and in turn handle their lifecycle action types. Consolidating all this logic into one file makes the file harder to read. I wrote an article on my approach to separating logic in your Redux Toolkit applications.

Dispatching actions in components

By using useSelector and useDispatch from react-redux, we can read state from a Redux store and dispatch any action from a component, respectively.

Let’s set up a component to dispatch getPosts when it mounts:

import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getPosts } from '../redux/features/posts/postThunk'

export default function Home() {
  const dispatch = useDispatch()
  const { entities, loading } = useSelector((state) => state.posts)

  useEffect(() => {
    dispatch(getPosts())
  }, [])

  if (loading) return <p>Loading...</p>

  return (
    <div>
      <h2>Blog Posts</h2>
      {entities.map((post) => (
        <p key={post.id}>{post.title}</p>
      ))}
    </div>
  )
}

The Redux DevTools extension gives real-time information on the dispatch of any lifecycle action type.

It’s important to note that payloadCreator accepts only two parameters, one of them being a custom argument that may be used in your request and the other being thunkAPI. thunkAPI is an object containing all of the parameters that are normally passed to a Redux Thunk function — like dispatch and getState. Take a look at all the acceptable parameters.

If your request requires more than one parameter, you can pass in an object when you dispatch the reducer function:

dispatch(getPosts({ category: 'politics', sortBy: 'name' })

Handling errors in createAsyncThunk

Remember that when your payloadCreator returns a rejected promise, the rejected action is dispatched (with action.payload as undefined). Most times, we want to display custom error messages rather than the message returned in the Error object.

By using thunkAPI, you can return a resolved promise to the reducer, which has action.payload set to a custom value of your choice. thunkAPI uses its rejectWithValue property to perform this.

Let’s say we want to add a new post to the blog. Our createAsyncThunk function would look something like this:

const post = { title: 'lorem', body: 'ipsum' }

const addPost = createAsyncThunk(
  'posts/addPost',
  async (post, { rejectWithValue }) => {
    try {
      const response = await fetch(
        'https://jsonplaceholder.typicode.com/posts',
        {
          method: 'POST',
          body: JSON.stringify(post),
          header: {
            'Content-Type': 'application/json',
          },
        }
      )
      const data = await response.json()
      return data
    } catch (err) {
      // You can choose to use the message attached to err or write a custom error
      return rejectWithValue('Opps there seems to be an error')
    }
  }
)

Then evaluate posts/addPost/rejected in extraReducers:

extraReducers: {
  [addPost.rejected]: (state, action) => {
    // returns 'Opps there seems to be an error'
    console.log(action.payload) 
  }
}

We’ve come to a close here, devs. So far we’ve been able to go over the basic features of createAsyncThunk and see how it works with the reducers in the slice function. The API also has some more advanced topics like canceling requests, which you can read further on.

Conclusion

To conclude, I’d like to mention Redux Toolkit’s RTK Query data fetching API.

RTK Query is a purpose-built, data-fetching and caching solution for Redux apps, which can eliminate the need to write any thunks or reducers to manage data fetching. So if you’ve dabbled with a library like React Query, it would be wise to use RTK Query for asynchronous logic in Redux because the syntax is quite similar.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Chinwike Maduabuchi Frontend developer passionate about software engineering.

Testing accessibility with Storybook

One big challenge when building a component library is prioritizing accessibility. Accessibility is usually seen as one of those “nice-to-have” features, and unfortunately, we’re...
Laura Carballo
4 min read

Leave a Reply