Redux Thunk: The Ultimate Guide

Redux is a powerful library for managing state in JavaScript applications, but out of the box, it only supports synchronous updates. What if you need to fetch data from an API or perform some other asynchronous operation? Enter Redux Thunk.

In this in-depth guide, we‘ll cover everything you need to know about Redux Thunk – what it is, how it works, and most importantly, how to use it effectively in your own projects. Whether you‘re a Redux beginner or a seasoned pro, by the end of this article you‘ll have a solid grasp of thunks and the ability to put them to work in real-world apps.

What is Redux Thunk?

Redux Thunk is a middleware that extends Redux‘s capabilities to support asynchronous actions. It allows you to write action creators that return thunks – functions that perform async work and dispatch actual actions when they‘re done.

Here‘s the key idea: instead of returning action objects, thunk action creators return functions that are executed by the Redux Thunk middleware. These thunks have access to the store‘s dispatch and getState methods and can use them to dispatch actions whenever they‘re ready, even asynchronously.

Without thunks, synchronous action creators return action objects like this:

function increment() {
  return {
    type: ‘INCREMENT‘
  }
}

And you‘d dispatch them like this:

store.dispatch(increment());

With thunks, you can write asynchronous action creators like this:

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      dispatch(increment());
    }, 1000);
  };
}

The thunk returned by incrementAsync is a function that takes dispatch as an argument. When you dispatch a thunk, the middleware executes it and passes in the store‘s dispatch:

store.dispatch(incrementAsync());

Inside the thunk, we can perform async operations like setTimeout, make AJAX requests, read and write to local storage – any side effects our app needs. We can also dispatch multiple actions over time.

Setting Up Redux Thunk

To use Redux Thunk in your project, first install it via NPM:

npm install redux-thunk

Then apply it as middleware when creating your Redux store:

import { createStore, applyMiddleware } from ‘redux‘; 
import thunk from ‘redux-thunk‘;
import rootReducer from ‘./reducers‘;

const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

That‘s it! With the thunk middleware applied, you can now write and dispatch thunk action creators in your app.

Thunk Examples

Let‘s look at some real-world examples of how you might use thunks. We‘ll start simple and build up to more complex use cases.

Basic Async Action

Here‘s an action creator that dispatches an increment action after a 1 second delay:

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      dispatch({ type: ‘INCREMENT‘ });
    }, 1000);
  };
}

To dispatch it:

store.dispatch(incrementAsync());

This is equivalent to calling setTimeout(() => store.dispatch({ type: ‘INCREMENT‘ }), 1000) directly, but it‘s a bit cleaner and more reusable written as a thunk.

Fetching Data from an API

A very common use case for thunks is fetching data from an API. Here‘s a thunk that fetches a user from a /api/user endpoint and dispatches success and failure actions:

function fetchUser(userId) {
  return async dispatch => {
    dispatch({ type: ‘FETCH_USER_REQUEST‘ });
    try {
      const response = await fetch(`/api/users/${userId}`);
      const user = await response.json();
      dispatch({ type: ‘FETCH_USER_SUCCESS‘, user });
    } catch (error) {
      dispatch({ type: ‘FETCH_USER_FAILURE‘, error });
    }
  };
}

This thunk first dispatches a FETCH_USER_REQUEST action to indicate that the request is starting. It then uses the fetch API to make an asynchronous request to the server.

If the request succeeds, the thunk dispatches a FETCH_USER_SUCCESS action with the user data. If it fails, it dispatches a FETCH_USER_FAILURE with the error.

Here‘s how you might dispatch this thunk:

store.dispatch(fetchUser(123));

And here are the corresponding reducers that handle the request lifecycle actions:

function userReducer(state = {}, action) {
  switch (action.type) {
    case ‘FETCH_USER_REQUEST‘:
      return {
        ...state,
        isFetching: true
      };
    case ‘FETCH_USER_SUCCESS‘:
      return {
        ...state,
        isFetching: false,
        user: action.user,
        error: null
      };
    case ‘FETCH_USER_FAILURE‘:
      return {
        ...state,
        isFetching: false,
        error: action.error
      };
    default:
      return state;
  }
}

This reducer responds to the actions dispatched by the fetchUser thunk to update the store with the appropriate loading, success, and failure states.

Chaining Async Actions

Thunks have access to the store‘s dispatch, so they can dispatch other thunks. This allows you to chain async actions together.

For example, you might have a thunk that first fetches a user, then fetches that user‘s posts:

function fetchUserAndPosts(userId) {
  return async dispatch => {
    await dispatch(fetchUser(userId));
    await dispatch(fetchPosts(userId));
  };
}

function fetchPosts(userId) {
  return async dispatch => {
    dispatch({ type: ‘FETCH_POSTS_REQUEST‘ });
    try {
      const response = await fetch(`/api/users/${userId}/posts`);
      const posts = await response.json();
      dispatch({ type: ‘FETCH_POSTS_SUCCESS‘, posts });
    } catch (error) {
      dispatch({ type: ‘FETCH_POSTS_FAILURE‘, error });
    }
  };
}

The fetchUserAndPosts thunk first dispatches the fetchUser thunk and waits for it to complete. It then dispatches the fetchPosts thunk to fetch that user‘s posts.

You can chain any number of async actions together this way. Each thunk can dispatch other thunks, which can dispatch other thunks, and so on.

Cancelling Async Actions

Sometimes you may want to cancel an async action that‘s in progress, like if the user navigates away from a page while a request is pending. Redux Thunk doesn‘t provide a built-in way to cancel thunks, but you can implement cancellation yourself with a little bit of code.

The basic idea is to pass a "cancel token" object into your thunks. The thunk checks this token periodically to see if it should abort. If the thunk is cancelled, it can dispatch a cancellation action and exit early.

Here‘s an example of a thunk that fetches posts, but can be cancelled:

function fetchPostsWithCancel(userId, cancelToken) {
  return async dispatch => {
    dispatch({ type: ‘FETCH_POSTS_REQUEST‘ });
    try {
      const response = await fetch(`/api/users/${userId}/posts`, {
        signal: cancelToken.signal
      });
      const posts = await response.json();
      if (!cancelToken.cancelled) {
        dispatch({ type: ‘FETCH_POSTS_SUCCESS‘, posts });
      }
    } catch (error) {
      if (!cancelToken.cancelled) {
        dispatch({ type: ‘FETCH_POSTS_FAILURE‘, error });  
      }
    }
    if (cancelToken.cancelled) {
      dispatch({ type: ‘FETCH_POSTS_CANCELLED‘ });
    }
  };
}

This thunk takes a cancelToken object as an argument in addition to the userId. Inside the thunk, it passes the token‘s signal property to the fetch call. This tells fetch to abort the request if the signal‘s aborted flag is set.

The thunk also checks the cancellation flag after the request completes. If the thunk was cancelled, it skips dispatching the success/failure actions and instead dispatches a FETCH_POSTS_CANCELLED action.

To use this thunk, you‘d create a cancel token and pass it in when dispatching:

const cancelToken = {
  cancelled: false,
  signal: new AbortController().signal
};

store.dispatch(fetchPostsWithCancel(123, cancelToken));

// Later, if you want to cancel:
cancelToken.cancelled = true;
cancelToken.signal.abortController.abort();

This code creates a cancel token with a cancelled flag and an AbortSignal. It dispatches the fetchPostsWithCancel thunk with the token.

Later, if it needs to cancel the request, it sets the token‘s cancelled flag and calls abort on the signal‘s controller. This triggers an abort event on the signal, which tells fetch to cancel the request.

The thunk detects the cancellation and dispatches a FETCH_POSTS_CANCELLED action instead of success or failure.

Testing Thunks

Thunks are just functions, so they‘re easy to test. You can call them with a mock dispatch and assert that they dispatch the right actions.

Here‘s how you might test the incrementAsync thunk from earlier:

describe(‘incrementAsync‘, () => {
  it(‘eventually dispatches an INCREMENT action‘, async () => {
    const dispatch = jest.fn();
    const thunk = incrementAsync();

    await thunk(dispatch);

    expect(dispatch).toHaveBeenCalledWith({ type: ‘INCREMENT‘ });
  });
});

This test creates a mock dispatch function with Jest. It calls the thunk with the mock dispatch and awaits the result. Finally, it asserts that dispatch was called with the expected action.

You can use a similar approach to test more complex thunks that make API requests. You‘ll need to mock fetch or whatever HTTP library you‘re using. You can then assert that the right requests are made and that the right actions are dispatched based on the response.

Thunks vs Other Async Libraries

Redux Thunk is just one way to handle async actions in Redux. There are other popular libraries like Redux Saga and Redux Observable that take different approaches.

Redux Saga uses ES6 generators to allow you to write async code that looks synchronous. Instead of dispatching thunks, you dispatch plain actions, which are then intercepted by sagas. The sagas can perform async operations and dispatch other actions in response.

Redux Observable uses RxJS observables to handle async actions. You dispatch actions to an "epic", which is like a souped-up reducer that can perform async operations and dispatch other actions.

Both Sagas and Observables offer more powerful tools for complex async workflows, like concurrency, cancellation, debouncing, throttling, and retrying. Thunks are a simpler, more lightweight approach. They‘re a good choice for most apps, but if you have very complex async needs, you might want to look into Sagas or Observables.

Thunk Best Practices

Here are a few tips for getting the most out of Redux Thunk:

  • Keep your thunks small and focused. Each thunk should do one thing and do it well. If a thunk is getting long or complex, consider breaking it up into smaller thunks.

  • Use action creators inside your thunks for dispatching actions. This keeps your action naming consistent and avoids duplication.

function fetchPosts(userId) {
  return async dispatch => {
    dispatch(fetchPostsRequest());
    try {
      const response = await fetch(`/api/users/${userId}/posts`);
      const posts = await response.json();
      dispatch(fetchPostsSuccess(posts));
    } catch (error) {
      dispatch(fetchPostsFailure(error));
    }
  };
}

function fetchPostsRequest() {
  return { type: ‘FETCH_POSTS_REQUEST‘ };
}

function fetchPostsSuccess(posts) {
  return { type: ‘FETCH_POSTS_SUCCESS‘, posts };
}

function fetchPostsFailure(error) {
  return { type: ‘FETCH_POSTS_FAILURE‘, error };
}
  • Use thunks for all async actions, even simple ones. This keeps your action creators pure and makes your code more consistent and easier to reason about.

  • In complex apps, consider organizing your thunks into separate files or folders by domain or feature. This makes them easier to find and maintain.

  • If you‘re using TypeScript, you can type your thunks and dispatch using the ThunkAction type from the redux-thunk package:

import { ThunkAction } from ‘redux-thunk‘;
import { RootState } from ‘./store‘;

type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

function fetchPosts(userId: number): AppThunk<Promise<void>> {
  return async dispatch => {
    // ...
  };
}

This ensures that your thunks are correctly typed and helps catch errors at compile time.

Conclusion

Redux Thunk is a powerful tool for handling async actions in Redux. It allows you to write action creators that return functions instead of objects, which can then perform async work and dispatch regular actions.

In this guide, we‘ve covered what thunks are, how to set them up, and how to use them to fetch data, chain actions together, and cancel requests. We‘ve also looked at how to test thunks and how they compare to other async libraries like Redux Saga and Redux Observable.

Armed with this knowledge, you should now be able to confidently use thunks in your own Redux apps to handle all your async needs. Happy coding!

Similar Posts