Register to get access to free programming courses with interactive exercises

Asynchronous requests (Thunk) React: Redux Toolkit

One of the most difficult tasks in building frontend applications is dealing with external requests. Difficulties come from two sides.

On the one hand, asynchrony itself generates ambiguities, so the standard mechanisms stop working. Redux doesn't know how to work in asynchronously, so all the processing of requests takes place outside redux. In this case, any non-trivial logic for processing asynchronous actions will appear inside React components.

const MyComponent = (props) => {
  const onClick = async (todoId) => {
    const response = await axios.get(`https://ru.hexlet.io/api/todos/${todoId}`);
    const todos = // Retrieve and convert data from response
    dispatch(todosLoaded(todos));
  };

  // render
};

On the other hand, the network is an unreliable thing, requests can take a long time or not be executed at all, and all this must be monitored to make sure you get the correct response. In the case of long requests, you need to show a spinner, and when the request breaks, you need to display a relevant message.

const onClick = async (todoId) => {
  // Here we work with the finite state machine to process any http request
  try {
    // Starting the loading process
    dispatch(todosLoadingStarted());
    const response = await axios.get(`https://hexlet.io/api/todos/${todoId}`);
    const todos = // extracting and transforming data from response
    dispatch(todosLoaded(todos));
  } catch (e) {
    // It's even more complicated, you have to keep track of exactly what went wrong
    dispatch(todosLoadingFailed(e.message));
  }
};

Even for a small number of calls, you would have to write a lot of the same or similar code. In real applications, the number of calls could be dozens or hundreds. Therefore, you can't do without a ready-made solution. To automate http requests, we need two mechanisms: the redux-thunk, middletrack, which is already included in Redux Toolkit, and the createAsyncThunk().

redux-thunk, is a piece of middleware that's added to Redux and allows asynchronous code within dispatch(). It's used to bring the logic of queries and storage updates into separate functions, called thunks:

// Note the nested function receiving dispatch
// This function will be stored outside the component, for example, in a slice
export const fetchTodoById = (todoId) => async (dispatch) => {
  const response = await axios.get(`https://hexlet.io/todos/${todoId}`);
  // here we need to perform the necessary normalization
  // and handle errors
  dispatch(todosLoaded(response.todos));
}

// usage
const TodoComponent = ({ todoId }) => {
  const dispatch = useDispatch();

  const onFetchClicked = () => {
    // Passed the asynchronous function
    dispatch(fetchTodoById(todoId));
  };

  // Somewhere here we use onFetchClicked
}

You can do roughly the same thing without redux-thunk just by writing an asynchronous function to which we pass dispatch() as input. The difference shows up in the more advanced use cases. For example, when we need to work with state or global objects, such as a websocket connection. In this case, you have to use redux-thunk, which makes this all much easier to do:

// (dispatch, getState, extraArgument)
export const fetchTodoById = (todoId) => async (dispatch, getState, extraArgument) => {
  // any data transmitted during the middleware configuration phase
  const { serviceApi } = extraArgument;
  const response = await serviceApi.getTodo(todoId);
  dispatch(todosLoaded(response.todos));
};

Despite the convenience gained, thunks themselves do not reduce the amount of code, and the same error handling will make up a large part of the code. This is where createAsyncThunk():

import { createAsyncThunk, createSlice, createEntityAdapter } from '@reduxjs/toolkit';
// To avoid hardcoding urls, we can make a module, within which URLs are created
import { getUserUrl } from './routes.js';

// Creating our Thunk
export const fetchUserById = createAsyncThunk(
  'users/fetchUserById', // displayed in dev tools and must be unique for everyone 
  async (userId) => {
    // Only query logic and data return here
    // No error handling
    const response = await axios.get(getUserUrl(userId));
    return response.data;
  }
);

const usersAdapter = createEntityAdapter();

const usersSlice = createSlice({
  name: 'users',
  // Adding loading process tracking to the state
  // { ids: [], entities: {}, loading: 'idle', error: null }
  initialState: usersAdapter.getInitialState({ loading: 'idle', error: null }),
  reducers: {
    // any reducers we need
  },
  extraReducers: (builder) => {
    builder
      // Called just before the query is executed
      .addCase(fetchUserById.pending, (state) => {
        state.loading = 'loading';
        state.error = null;
      })
      // Called if the request has been successfully executed
      .addCase(fetchUserById.fulfilled, (state, action) => {
        // Adding a user
        usersAdapter.addOne(state, action.payload);
        state.loading = 'idle';
        state.error = null;
      })
      // Called in case of an error
      .addCase(fetchUserById.rejected, (state, action) => {
        state.loading = 'failed';
        // https://redux-toolkit.js.org/api/createAsyncThunk#handling-thunk-errors
        state.error = action.error;
      });
  },
})

// Somewhere in the application
import { fetchUserById } from './slices/usersSlice.js';

// Inside the component
dispatch(fetchUserById(123));

Each Thunk created with createAsyncThunk(), contains three reducers: pending, fulfilled and rejected. They correspond to promise states and are called by Redux Toolkit the moment the promise enters one of these states. We don't have to respond to them all, we choose what's important to us in the application.


Recommended materials

  1. How Redux Thunk Works
  2. createAsyncThunk documentation
  3. RTK

Hexlet Experts

Are there any more questions? Ask them in the Discussion section.

The Hexlet support team or other students will answer you.

About Hexlet learning process

For full access to the course you need a professional subscription.

A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.

Get access
130
courses
1000
exercises
2000+
hours of theory
3200
tests

Sign up

Programming courses for beginners and experienced developers. Start training for free

  • 130 courses, 2000+ hours of theory
  • 1000 practical tasks in a browser
  • 360 000 students
By sending this form, you agree to our Personal Policy and Service Conditions

Our graduates work in companies:

<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.bookmate">Bookmate</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.healthsamurai">Healthsamurai</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.dualboot">Dualboot</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.abbyy">Abbyy</span>
Suggested learning programs

From a novice to a developer. Get a job or your money back!

Frontend Developer icon
Profession
beginner
Development of front-end components for web applications
start anytime 10 months

Use Hexlet to the fullest extent!

  • Ask questions about the lesson
  • Test your knowledge in quizzes
  • Practice in your browser
  • Track your progress

Sign up or sign in

By sending this form, you agree to our Personal Policy and Service Conditions
Toto Image

Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.