Cancelling network requests using Harlem actions
Tags: vue, typescript, statemanagement
Published: 2021-11-10 07:03:23

Cancelling asynchronous tasks isn't necessarily a topic that comes up all too often in the JavaScript world but anyone who has ever attempted to come up with an elegant cancellation pattern will know it's a little more difficult to implement than it seems on the surface.

In a lot of cases not cancelling stale asynchronous tasks has few consequences, but in that case of network requests this can lead to unexpected behaviour such as race conditions.

This issue can be further complicated when network requests are spawned from a state management system where the result of the request is assigned to state. If uncancelled requests are allowed to complete, they may finish in an order you may not anticipate and thus corrupt your application state.

Thankfully these days we have the [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) which can be used as a cancellation token for things like network requests and DOM event listeners.

In this article I will explain how [Harlem](https://harlemjs.com) leverages the [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) to create a robust and elegant async cancellation pattern through the use of actions.

## What is Harlem
I won't go into a great amount of detail here but in a nutshell Harlem is a simple, unopinionated, lightweight and extensible state management solution for Vue 3. It's suitable for applications and architectures of all sizes, and if you're a TypeScript user then you will feel right at home.

Head on over to the [Harlem docs](https://harlemjs.com) to learn more or check out a demo of it in action [here](https://andrewcourtice.github.io/harlem).

## Installation
Let's start by installing a few packages:
```bash
npm install @harlem/core @harlem/extension-action
# or
yarn add @harlem/core @harlem/extension-action
```
Here's a quick summary of the packages we just added:
- **@harlem/core**: this is the main Harlem package for creating and using a store in Vue
- **@harlem/extension-action**: this extension adds async actions to your store

Harlem has a good selection of official extensions you can use to extend your stores with some powerful features such as cancellable actions, async getters, local/session storage sync, tracing etc.

*You can also add the devtools plugin (@harlem/plugin-devtools) for inspecting your stores during development if you wish.*

See [here](https://harlemjs.com/extensibility/extensions/introduction.html#official-extensions) for the full list of official extensions and plugins for Harlem.

Once the above packages are installed you'll need to register Harlem with your Vue application:
```typescript
import App from './app.vue';
import Harlem from '@harlem/core';
import devtoolsPlugin from '@harlem/plugin-devtools'; // Optional

createApp(App)
    .use(Harlem, {
        plugins: [
            devtoolsPlugin() // Optional
        ]
    })
    .mount('#app');
```

## Creating a simple store
Once you've installed the packages and registered Harlem with your app it's time to create a store. Create a `store.ts` (or js) file somewhere in your app and add the following:
```typescript
import actionExtension from '@harlem/extension-action';

import {
    createStore
} from '@harlem/core';

const STATE = {
    people: []
};

export const {
    state,
    getter,
    mutation,
    action,
} = createStore('starwars', STATE, {
    extensions: [
        actionExtension()
    ]
});
```
For more information on stores, getters, mutations etc. check out the [Harlem docs](https://harlemjs.com/guide/core-concepts/stores.html#defining-a-store).

## Creating an action
Once we've got our store we can now add our action:
```typescript
export const loadStarwarsCharacters = action('load-starwars-characters', async (_, mutate, controller) => {
    const response = await fetch('https://swapi.dev/api/people', {
        signal: controller.signal
    });

    const characters = await response.json();

    mutate(state => {
        state.characters = characters;
    });
});
```
Notice how we assign `controller.signal` to the `signal` property to the body of the fetch request.

Internally Harlem creates a new instance of an `AbortController` each time the action is run so that when an instance of the action is cancelled, any child actions or network requests can be synchronised and cancelled as well. See [here](https://github.com/andrewcourtice/harlem/blob/main/packages/task/src/index.ts) for how Harlem uses the `AbortController` and makes it compatible with JavaScript's native `async/await` syntax.

The default behaviour for Harlem actions is to cancel running instances of actions upon subsequent calls. In other words, if you run the action 3 times consecutively and the first 2 instances haven't completed by the time the 3rd instance is started, Harlem will automatically abort the first 2.

To change this behaviour simply specify `parallel: true` in the action options like so:
```typescript
export const loadStarwarsCharacters = action('load-starwars-characters', async (_, mutate, controller) => {
    // Our action body
}, {
    parallel: true
});
```

And that's all there is to it. You've now got a simple store that can handle complex async workflows as a first-class citizen as the application scales up.

For more information about actions in Harlem, how to check their status, advanced cancellation patterns or child actions, check out the [actions extension documentation](https://harlemjs.com/extensibility/extensions/action.html).

Have a great day and happy coding :)