Writing Thunks using createAsyncThunk in React Redux

We can write thunks that dispatch “loading”, “request succeeded”, and “request failed” actions. We had to write action creators, action types, and reducers to handle those cases.

Because this pattern is so common, Redux Toolkit has a createAsyncThunk API that will generate these thunks for us. It also generates the action types and action creators for those different request status actions, and dispatches those actions automatically based on the resulting Promise.

booksSlices.ts

import {createAsyncThunk, createEntityAdapter, createSlice} from "@reduxjs/toolkit";
import {RootState} from "../../app/store";

export type BookState = { bookId: number, bookTitle: string, };

export const addOneBookAsync = createAsyncThunk(
    "books/addOneBookAsync",
    async (bookTitle: string) => {
        let result;
        setTimeout(result = function () {
            const newBook: BookState = {bookId: Math.floor(Math.random() * 1000), bookTitle: bookTitle};
            return newBook;
        }, 1000);
        return result();
    }
);

const booksAdapter = createEntityAdapter<BookState>({
    selectId: model => model.bookId,
    sortComparer: (a, b) => a.bookTitle.localeCompare(b.bookTitle),
});

export const booksSlice = createSlice({
    name: "books",
    initialState: booksAdapter.getInitialState,
    reducers: {
        addOne: booksAdapter.addOne,
    },
    extraReducers: builder => {
        builder
            .addCase(addOneBookAsync.pending, state => {
                // we can update state while loading data
                console.log("loading");
            })
            .addCase(addOneBookAsync.fulfilled, (state, action) => {
                // we can update state after successful response or we can call another reducer
                // here we are going to call another reducer
                booksSlice.caseReducers.addOne(state, action);
                console.log("successful");
            }).addCase(addOneBookAsync.rejected, state => {
            // we can update state after failed
            console.log("failed");
        })
    }
});

export const booksActions = booksSlice.actions;
export const booksSelectors = booksAdapter.getSelectors<RootState>(
    (state) => state.books
)

export default booksSlice.reducer;

Books.tsx

import {useAppSelector, useAppDispatch} from '../../app/hooks';
import {BookState, booksActions, booksSelectors, addOneBookAsync} from "./booksSlice";
import {useState} from "react";

function Books() {

    const dispatch = useAppDispatch();
    const books = useAppSelector(booksSelectors.selectAll);
    const [bookTitle, setBookTitle] = useState("");

    const addHandler = () => {
        const newBook: BookState = {
            bookId: Math.floor(Math.random() * 1000),
            bookTitle: bookTitle
        };
        dispatch(booksActions.addOne(newBook));
    }

    const addAsyncHandler = async () => {
        try {
            const result = await dispatch(addOneBookAsync(bookTitle)).unwrap();
            console.log(result);
        } catch (rejectedValueOrSerializedError) {
            // handle error here
        }
    }

    return (
        <div>

            <div className="mx-2 my-2">
                <input type="text" placeholder="Books Title" className="input input-bordered w-full max-w-xs"
                       value={bookTitle} onChange={event => setBookTitle(event.target.value)}/>
                <button className="btn mx-2" onClick={addHandler}>Add</button>
                <button className="btn mx-2" onClick={addAsyncHandler}>Add Async</button>
            </div>

            <ul>
                {books.map((value, index) => (
                    <li key={value.bookId}>Title : {value.bookTitle}, Id : {value.bookId}</li>
                ))}
            </ul>
        </div>
    );
}

export default Books;

Return Value

createAsyncThunk returns a standard Redux thunk action creator. The thunk action creator function will have plain action creators for the pendingfulfilled, and rejected cases attached as nested fields.

Handling Thunk Results​

The promise returned by the dispatched thunk has an unwrap property which can be called to extract the payload of a fulfilled action or to throw either the error or, if available, payload created by rejectWithValue from a rejected action:

const onClick = () => {
  dispatch(fetchUserById(userId))
    .unwrap()
    .then((originalPromiseResult) => {
      // handle result here
    })
    .catch((rejectedValueOrSerializedError) => {
      // handle error here
    })
}

Or with async/await syntax:

const onClick = async () => {
  try {
    const originalPromiseResult = await dispatch(fetchUserById(userId)).unwrap()
    // handle result here
  } catch (rejectedValueOrSerializedError) {
    // handle error here
  }
}

References
https://redux-toolkit.js.org/api/createAsyncThunk
https://redux.js.org/tutorials/fundamentals/part-8-modern-redux#writing-thunks
https://redux-toolkit.js.org/api/createslice#extrareducers
https://stackoverflow.com/questions/65106681/redux-toolkit-dispatch-an-action-from-an-extrareducers-listener