Working With An API In Aurelia Store

Unless you’re working on a completely static web application, chances are you’ll be needing to get data from an API or send data to one. In Aurelia, you would usually do this either using the HTTP Client or Fetch Client (you should be using Fetch). However, what about Aurelia Store? The versatile state management library for Aurelia.

You have two approaches you can take when using Aurelia Store with an API, and we’ll discuss the pros and cons of each one. This article is going to be a small and relatively non-technical article.

Make API requests from within your actions

Aurelia Store supports asynchronous actions, meaning you can delay their execution until the async function promise resolves. A simple Aurelia store action might look like this if using async.

async function myAction(state) {
    const newState = { ...state };

    // Do something in here

    return newState;
}

When you return from within an async function, it’s akin to a Promise.resolve the entire function itself becomes wrapped in a promise, but we’re not going to go into specifics of how async/await works in this post.

Making a function async means, you can call an API from within your action and then store the result in your state when it loads. This simplified code assumes that you’re accounting for errors in the loadUsers, hence there not being a try/catch block below.

async function myAction(state) {
    const newState = { ...state };

    newState.users = await api.loadUsers();

    return newState;
}

This approach works well because it means you can load data through actions, they are easier to debug, and you can perform transformations on the data returned. It also makes refactoring your code so much easier as you only need to change one action.

However, the downside is if the request above takes longer than expected. Maybe you’re using an Azure or Google Cloud Function (lambda function), and it takes a few seconds to start because it’s not warmed up, this request might take a few seconds to complete and meanwhile, your application slows down to a crawl. If you’re calling multiple actions, it’ll delay calling the other actions until the current one returns a state.

If you keep an eye on your actions and ensure the requests are tiny, then this should never be a problem. Adding a visual loading indicator into the application can also alleviate any loading issues. However, assumption-led decision making can sometimes backfire.

API Call First, Dispatch Action Second

In the above approach, the request and action are one function. In this approach, you request the data first and then dispatch an action after.

There is an obvious upside to this approach. Your actions execute fast; they don’t have to wait for API calls to resolve before continuing. Another upside is that it ensures your state layer and API logic remains separate. The maybe not-so-obvious downside is you now have to write and maintain two different pieces of code.

async function myAction(state, users) {
    const newState = { ...state };

    newState.users = users;

    return newState;
}

async function loadUsers() {
    const request = await fetch('/users/all');
    const response = await request.json();

    return response;
}

export class ViewModel {
    async activate() {
        const users = await loadUsers();
        dispatchify(myAction)(users);
    }
}

But, which one do I use?

The point of this post is not to tell you that you should use one over the other; it is to point out that the two approaches have upsides and downsides. Either all is fine, you only need to be aware of the caveat of async actions. When you create an asynchronous function, they all become async.

I use the second approach in my Aurelia applications. I like decoupling my state management from my API logic completely. It means if I want to remove state management at a later date or use something else, there is no tight coupling in my code. Remove my actions and call the API functions myself from my view-models (or wherever).

Leave a Reply

Your email address will not be published. Required fields are marked *