Thunks and Redux Toolkit

Get started with thunks and Redux toolkit.

Introduction

Do you remember what “thunks” are? Here’s a quick reminder.

When you first learn Redux, you were likely taught that you must dispatch an action to make a state change and that the action has to be an object. Usually, it is in the following form:

{
  type: "ACTION_TYPE",
  payload: {}
}

Well, that’s not entirely true.

In the typical flow of a Redux app, the reducer is the only actor that requires the action to be an object!

This means you can dispatch whatever Javascript data type you want but must transform it into an object before it reaches the reducers.

This is the entire premise on which thunks are built.

By dispatching a “function” (aka thunk), a middleware could act on it. It could even pass it special arguments.

So, what is a “thunk”?

Generally speaking, a thunk is a function that wraps an expression to delay its evaluation.

If you choose to return a function from an action creator, we can safely call that a thunk.

// before:
function fetchTweetsAction () {
	//return an action object
	return {
	        
  }
}

// now:
function fetchTweetsAction () {
    // return a function 
  	return function() {

  }
}

How does this help us at all?

We’re going to create a thunk to handle the fetching of tweets.

First, whenever a user clicks the search button, we will dispatch a thunk. Remember that this is just a function.

Now here comes the interesting bit. The dispatched function will be passed the dispatch and getState Redux store methods. This is what makes thunks powerful.

function fetchTweetsAction () {
    // receives dispatch and getState arguments 
  	return function(dispatch, getState) {

  }
}

Within the thunk, we will then perform the actual data fetch and dispatch actions to denote loading and success states.

Let’s see how that works in code by creating a new thunk called fetchTweets. We’ll do this in the finderSlice.js file, keeping every state-related logic in one place.

export const fetchTweets = (searchValue, numberOfResults) => async (dispatch) => {
  
};

Note how this takes the searchValue and numberOfResults arguments and returns an async function that receives the store dispatch method. That’s a thunk!

Under the hood, redux-toolkit sets up a middleware that will intercept the dispatched function and invoke it with the store’s dispatch and getState methods. We haven’t used getState here, as we don’t need it.

When it is time to dispatch this thunk, it’ll be invoked like this:

// dispatch with searchValue and numberOfResults
dispatch(fetchTweets('search value', 10))

Within the thunk, we will now make the actual data fetch and dispatch some actions while we’re at it:

// import the function that calls the Twitter API
import { findTweets } from "./findTweets";

export const fetchTweets = (searchValue, numberOfResults) => async (dispatch) => {
    // loading ...
    dispatch(isLoadingTweets());

    // perform actual data fetch 
    const tweets = await findTweets(searchValue, numberOfResults);

    // success 
    dispatch(loadingTweetsSuccess(tweets));
};

In the code block above, findTweets refers to the actual fetch handler that we created in findTweets.js.

But where do these actions come from? Let’s add those to the finderSlice slice:

// before 
const finderSlice = createSlice({
  name: "finder",
  initialState,
  reducers: {
    addTweet(state, payload) {
      state.tweets = payload;
    },
  },
});

Remember that the createSlice utility creates the reducer and action creators on our behalf. Let’s take advantage of this to create the isLoadingTweets and loadingTweetsSuccess action creators.

First, let’s change the initialState of the slice to hold more information, e.g., loading state.

// before 
const initialState = { tweets: [] };

// now 
const initialState = { tweets: [], isLoading: false, error: null };

We’ll consider error handling later, so let’s have it in the initial state as null.

Now, we can update the slice implementation as follows:

...

const finderSlice = createSlice({
  name: "finder",
  initialState,
  reducers: {
    // note that the payload is deconstructed from the action object
    loadingTweetsSuccess(state, {payload}) {
      state.tweets = payload;
      state.isLoading = false;
      state.error = null;
    },
    isLoadingTweets(state) {
      state.isLoading = true;
    },
  },
});

Easy to reason about!

Now, we just need to pull out the action creators from the returned slice object:

// before 
export const { addTweet } = finderSlice.actions;

// now 
export const { isLoadingTweets, loadingTweetsSuccess } = finderSlice.actions;

If you followed the steps above, you’d have successfully created a thunk that is ready to be dispatched!

Let’s now move on to the next lesson where we perform the actual thunk dispatch, and change the UI to handle the loading state appropriately.

Get hands-on with 1200+ tech skills courses.