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:
- dispatch action after fetching has begun
- dispatch action if fetching has failed
- 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
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: