redux-async-adapter

A simple redux toolkit adapter to handle loading states for async thunks. Inspired by Redux Toolkit's createEntityAdapter and is designed to work with Redux Toolkit's createAsyncThunk.

Usage no npm install needed!

<script type="module">
  import reduxAsyncAdapter from 'https://cdn.skypack.dev/redux-async-adapter';
</script>

README

Redux Async Adapter

A simple redux toolkit adapter to handle loading states for async thunks. Inspired by Redux Toolkit's createEntityAdapter and is designed to work with Redux Toolkit's createAsyncThunk.

Overview

A function that generates a set of reducers and selectors for keeping track of loading state (loading, loaded, error, lastLoaded) for different async operations. It supports multiple loading statuses within a state.

The methods generated by createAsyncAdapter will all manipulate an "async state" structure that looks like:

import { SerializedError } from '@reduxjs/toolkit';

export interface AsyncState<T> {
  status: { [name: string]: AsyncStatus | undefined };
  data: T;
}

export interface AsyncStatus {
  name: string;
  loading: boolean;
  loaded: boolean;
  error: SerializedError | undefined;
  lastLoaded: string | undefined;
}

createAsyncAdapter can be called multiple times within an application and can handle async thunks created by createAsyncThunk

Example

import createAsyncAdapter from 'redux-async-adapter'
import { configureStore, createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// create the adapter
const asyncAdapter = createAsyncAdapter()

// create an async thunk that load books
const fetchBooks = createAsyncThunk(
  'books/fetch',
  async () => {
    // fetch books from some api
    return ['book 1', 'book 2']
  }
)

const booksSlice = createSlice({
  name: 'books',
  initialState: asyncAdapter.getInitialState([])
  reducers: {},
  extraReducers: {
    // use the handler directly
    [fetchBooks.pending.type]: asyncAdapter.handlePending(fetchBooks),

    // use the handler as part of a reducer
    [fetchBooks.fulfilled.type]: (state, action) => {
      asyncAdapter.handleFulfilled(fetchBooks)(state)
      state = action.payload
    },

    [fetchBooks.rejected.type]: asyncAdapter.handleRejected(fetchBooks),
  }
})

const store = configureStore({
  reducer: {
    books: booksSlice.reducer
  }
})

// use the selectors to get the thunk's status
const fetchBooksStatus =
  asyncAdapter
    .getSelectors()
    .selectAsyncStatus(store.getState(), fetchBooks)

// access various statuses
const {loading, error, loaded, lastLoaded} = fetchBooksStatus

Parameters

createAsyncAdapter accepts an optional options parameter which an an object containing the configuration for the adapter.

Options

  • usePayloadAsError boolean?: use the payload field of a rejected action instead of the error field. This is useful if you are using rejectWithValue in your async thunk
  • onFulfilled function?: a function that receives an AsyncStatus and returns an AsyncStatus that is run after handleFulfilled
  • onPending function?: a function that receives an AsyncStatus and returns an AsyncStatus that is run after handlePending
  • onRejected function?: a function that receives an AsyncStatus and returns an AsyncStatus that is run after handleRejected
  • onReset function?: a function that receives an AsyncStatus and returns an AsyncStatus that is run after handleReset
adapter.handlePending(thunk)(state);

Return Value

createAsyncAdapter returns an object that looks like this:

{
  getInitialState: <T>(initialData: T) => AsyncState<T>,
  getSelectors: () => ({
    selectStatus: (state: AsyncState, thunk: AsyncThunk) => AsyncStatus,
    selectAllStatuses: (state: AsyncState) => AsyncStatus[],
    selectAnyLoading: (state: AsyncState) => boolean,
    selectAllErrors: (state: AsyncState) => SerializedError[],
    selectAllFinished: (state: AsyncState) => boolean
  }),
  handlePending: (thunk: AsyncThunk) => (state: AsyncState) => void,
  handleFulfilled: (thunk: AsyncThunk) => (state: AsyncState) => void,
  handleRejected: (thunk: AsyncThunk) => (state: AsyncState, action: AsyncThunk['rejected']) => void,
}

State

  • getInitialState: accepts a initialData parameter of any type and return an AsyncState with that initial data and an empty status array
const initialState = adapter.getInitialState({});
// initialState is { status: {}, data: {} }

Handlers

All handlers accept an asyncThunk (created by redux toolkit's createAsyncThunk) and return a reducer that can be used to update the status for that thunk.

  • handlePending: accepts an asyncThunk and return a reducer that reset error and loaded state and set loading to true for that thunk.
adapter.handlePending(thunk)(state);
  • handleFulfilled: accepts an asyncThunk and return a reducer that reset error and loading state and set loaded to true and update lastLoaded timestamp (ISO formatted)
adapter.handleFulfilled(thunk)(state);
  • handleRejected: accepts an asyncThunk and return a reducer that reset loaded and loading state and update the error field with the error provided in the action
adapter.handleRejected(thunk)(state, action);
  • handleReset: accepts an asyncThunk and return a reducer that reset the loading and loaded state to false and reset the error and last loaded fields
adapter.handleReset(thunk)(state);
  • resetAllStatuses: a reducer that reset all of the loading statuses previously stored
adapter.resetAllStatuses(state);

Selectors

  • selectData: select the data in the state (e.g. the data that you passed when using getInitialState)
adapter.getSelectors().selectData(state);
  • selectStatus: accepts an asyncThunk (created by redux toolkit's createAsyncThunk) and returns a selector that accepts a state and returns the status object of that particular thunk
adapter.getSelectors().selectStatus(thunk)(state);
  • selectAllStatuses: returns an array of all status objects within the state
adapter.getSelectors().selectAllStatuses(state);
  • selectAnyLoading: returns whether or not any thunks within the state is currently loading
adapter.getSelectors().selectAnyLoading(state);
  • selectAllErrors: returns a list of all the errors in the state
adapter.getSelectors().selectAllErrors(state);
  • selectAllFinished: returns whether or not all thunk in the state has finished (loaded)
adapter.getSelectors().selectAllFinished(state);