Type-safe Asynchronous Actions (Redux Thunk) Using TypeScript FSA

Type-safe Asynchronous Actions (Redux Thunk) Using TypeScript FSA

If you haven't read the previous blog post (which describes FSA and the TypeScript FSA library), please read it before continuing. In this article, I cover the creation and usage of Typesafe actions and the way to unit test reducers that use asynchronous actions.

Before going any deeper

I'll explain a few concepts before going into the TypeScript FSA and async actions.

I use Redux Thunk in this blog post, but you can use another library (such as Redux Saga). You may have realized that you don't need any of these in your project.

Redux Thunk is middleware for Redux. Redux Thunk adds support for handling action creators that return function instead of object literals.

"Thunk" refers to wrapping expressions with functions to delay expression evaluation.

Redux Thunk provides a dispatcher for asynchronous actions, allowing actions to be dispatched step-by-step during asynchronous processes.

Creating an asynchronous action with TypeScript FSA

Asynchronous action creators look like this:

import actionCreatorFactory from 'typescript-fsa'
const actionCreator = actionCreatorFactory()

export const loadAirplane = actionCreator.async<number,
                                                LoadAirplaneResponse, 
                                                {}>('AIRPLANE_LOADING')

The first step is importing actionCreatorFactory from typescript-fsa. Using the factory, I return a new action creator.

Is using a factory pattern adding unnecessary complexity for a small libray? No. The actionCreatorFactory takes parameters (such as prefixes) that can automatically provide certain contexts in a large application (such as a product). All actions related to the context will automatically have the prefix.

I then use the action creator to create an async action. To define an action, I need to provide three items: payload (in the example, type of number), success data (LoadAirplaneResponse), and fail data (any). Finally, I call the action creator with the action name ('AIRPLANE_LOADING').

The async action creator creates three different actions automatically:

airplaneLoading.started(1)
airplaneLoading.done({
  params: 1,
  result: { }, // <- type of LoadAirplaneResponse
})
airplaneLoading.failed({
  params: 1,
  error: {},    
})

I can use these events in my components at this point, but that doesn't get us far. I need an asynchronous job to make this more usable. The function that does the job is called worker.

Creating an asynchronous job with promises

Fetching data from the server is a common asynchronous job. The fetching is typically done with help of a library (jQuery AJAX, Axios etc.) that will provide an abstraction on top of XMLHTTPRequest. You can use a native fetch API on a modern browser.

Sending requests is so common that it is wise to create a helper that will do following:

  1. dispatch action after fetching has begun
  2. dispatch action if fetching has failed
  3. dispatch action after fetching is successfully completed

The GitHub Gist below contains an example implementation of such a helper.

Embedded JavaScript

The idea of this helper is to provide the actions and a promise-based task when calling the function.

export const loadAirplaneWorker =
  wrapAsyncWorker(loadAirplane, (airplaneId): Promise<LoadAirplaneResponse> => getAirplane(airplaneId))

The promise (getAirplane(airplaneId)) in the example fetches the airplane with airplaneId. The helper will automatically send the action AIRPLANE_LOAD_STARTED when fetching begins, AIRPLANE_LOAD_FAILED when fetching fails, and AIRPLANE_LOAD_DONE when data has been fetched successfully.

To start fetching, I need to call my component.

interface AirplaneDisplayProps {
  loadAirplane: (airplaneId:number) => Promise<any>
}

export class AirplaneDisplay extends 
  React.Component<AirplaneDisplayProps, void> {

  componentDidMount() {
    this.props.loadAirplane(1)
  }

  render() {
    return <div></div>
  }
}

const mapStateToProps = () => {
  ...do your thing here
}

const mapDispatchToProps = (dispatch,
                            props:AirplaneDisplayProps):AirplaneDisplayProps 
=> ({
  ...props,
  loadAirplane: (airplaneId:number) => 
    loadAirplaneWorker(dispatch, airplaneId)
})

export default connect<AirplaneDisplayProps,
                       AirplaneDisplayProps, 
                       any>
  (mapStateToProps, mapDispatchToProps)(AirplaneDisplay)

Note: I call the worker with dispatch. Dispatch is required to dispatch actions in different phases during the asynchronous process.

I have now defined asynchronous actions and the worker that will dispatch these actions. The next step is to create a logic into the reducer that will modify the state based on the action.

Handling asynchronous actions in the reducer

In the previous blog post, I had examples of handling synchronous actions. One of the examples was

TypeScript FSA reducer synchronous example

The basic idea is exactly the same for asynchronous actions, except that there are three actions to handle. I created an example reducer to show how to make a Typesafe reducer that will modify the state of the Airplane entity.

import { Action } from 'redux'
import { isType } from 'typescript-fsa'
import { loadAirplane } from '../actions'

interface Airplane {
}

export default function airplaneReducer(airplane: Airplane | null, 
                                        action: Action): Airplane | null {
  if (isType(action, loadAirplane.started)) {
    // show spinner or something
  }

  if (isType(action, loadAirplane.done)) {
    // set airplane to the state, hide spinner, etc.
  }

  if (isType(action, loadAirplane.failed)) {
    // show error message, hide spinner, etc.
  }

  return airplane
}

In the comment sections, I described what could happen in each case. Normally, I would have separate reducers, for example, changing spinner state would be in it's own reducer. For the sake of simplicity, however, I used a single file.

As a bonus, I have a GIF animation that shows how things look from the developer's perspective:

Autocompletion of TypeScript FSA action in reducer