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 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 leverages the 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 to learn more or check out a demo of it in action here.
Installation
Let's start by installing a few packages:
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 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:
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:
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.
Creating an action
Once we've got our store we can now add our action:
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 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:
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.
Have a great day and happy coding :)
Top comments (0)