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.
The Hexlet support team or other students will answer you.
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.
Programming courses for beginners and experienced developers. Start training for free
Our graduates work in companies:
From a novice to a developer. Get a job or your money back!
Sign up or sign in
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.