Codecademy Logo

Async Actions with Middleware and Thunks

Thunks

A thunk is a function used to delay a computation until it is needed by an application. The term thunk comes from a play on the word “think” but in the past tense.

In JavaScript, functions are thunks since they hold a computation and they can be executed at any time or passed to another function to be executed at any time.

A common practice is for thunks to be returned by a higher-order function. The returned thunk contains the process that is to be delayed until needed.

const alarmOne = () => {
console.log("Wake Up!!!");
};
alarmOne(); // "Wake Up!!!"
const getAlarmThunk = () => {
return () => {
console.log("Wake Up!!!");
}
};
const alarmTwo = getAlarmThunk();
alarmTwo(); // "Wake Up!!!"

Middleware In Redux

Redux middleware extends the store’s abilities and lets you write asynchronous logic that can interact with the store. Middleware is added to the store either through createStore() or configureStore().

Redux Thunk Middleware

Redux Toolkit’s comes with a utility function, createAsyncThunk(), which uses middleware and thunks internally.

import { createAsyncThunk } from "@reduxjs/toolkit";
import { fetchTodos } from '../actions';
const loadTodos = createAsyncThunk("todos/loadTodos",
// The thunk to be executed asynchronously
async () => {
const resp = await fetchTodos();
return await resp.json()
})

Redux Toolkit’s configureStore()

The Redux Toolkit (@reduxjs/redux-toolkit) package’s configureStore() includes a thunk middleware by default.

The thunk middleware is used internally, like in createAsyncThunk(), when interacting with the store.

`createAsyncThunk()

createAsyncThunk() accepts a Redux action type string and a callback function that should return a promise. It generates promise lifecycle action types based on the action type prefix that you pass in, and returns a thunk action creator that will run the promise callback and dispatch the lifecycle actions based on the returned promise.

The callback function takes a user-defined data argument and a thunkAPI object argument. The data argument is originally sent as an argument to the thunk action creator where an object can be used if multiple points of data are necessary. The thunkAPI object contains the usual thunk arguments, such as dispatch and getState.

import { createAsyncThunk } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
const fetchUser = createAsyncThunk(
'users/fetchByIdStatus',
async (user, thunkAPI) => {
const response = await userAPI.fetchById(user.id)
return response.data
}
)
const user = {username: "coder123", id: 3};
store.dispatch(fetchUser(user))

extraReducers Property

The object passed to createSlice() may contain a fourth property, extraReducers, which allows createSlice() to respond to other action types besides the types it has generated. This is useful when handling asynchronous logic using createAsyncThunk.

The logic within extraReducers that acts on the slice of state can safely use mutatable updates because it uses Immer internally.

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { client } from '../api';
const initialState = {
todos: [],
status: 'idle'
};
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/todosApi/todos');
return response.todos;
});
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action) => {
state.todos.push(action.payload);
}
},
extraReducers: {
[fetchTodos.pending]: (state, action) => {
state.status = 'loading';
},
[fetchTodos.fulfilled]: (state, action) => {
state.status = 'succeeded';
state.todos = state.todos.concat(action.payload);
}
}
});

Learn more on Codecademy