[en] Promises in Vue – how to do api requests correctly

Polish version

On this article I assume, that you have knowlege about promises in JS. If not, read MDN article.

Look at this vue component and try to find bug in it:

<template>
    <div>
        <button @click="load(1)" :class="{active:active===1}">Show first article</button>
        <button @click="load(2)" :class="{active:active===2}">Show second article</button>
        <p v-if="loader">Loading...</p>
        <p v-else>{{content}}</p>
    </div>
</template>
<script>
export default {
    data(){
        return {content:null, active:null, loader:false}
    },
    methods:{
        load(id){
            this.active=id;
            this.loader=true;
            fetch(`/article/${id}`)
                .then(response=>response.json())
                .then(json=>{
                    this.content=json;
                    this.loader=false;
                });
        }
    }
};
</script>
<style>
    button.active{
        color:red;
    }
</style>

It is simple component, that has 2 buttons. After clicking on button it loads content from rest api and adds class active so you can see which article is opened. It even has message ‘Loading…’ until content was loaded. It is so simple, what could be wrong here?

But what will it do, when you try click both buttons quickly? Probably nothing, because you probably testing it on localhost, so api responds faster that you click. And even if it is slower (or you simulate slow network in dev tools) probably nothing happens.

It is because server responses in the same order that you clicked. But it is possible, that processing of first request will take more time, that the second. Then your content variable don’t match active variable, so for user content of article don’t match selected button.

You can think, so if works correctly most of time, that it is not worth to carry. But in realiy this is the worst kind of bug: user/customer says, taht sometimes your softwarre is buggy, but when you tries, it works complettly fine. But customer don’t stop complaining.

My method how to reproduce such bug is to add random sleep to all requests on server, so all responses will be received in random order.

How to fix it

There are many ways to fix it. The simplest could be, to disable buttons when loader is true. But this isn’t user-friendly and for me it is hiding bug, not fixing.

Second idea is to test if this request is still valid

        load(id){
            this.active=id;
            this.loader=true;
            fetch(`/article/${id}`)
                .then(response=>response.json())
                .then(json=>{
                    if(this.active!=id) return;//id changed, so ignore
                    this.content=json;
                    this.loader=false;
                });
        }

In this example we tested if id was changed, because we assume that if we ask api multiple times for the same id we get the same response. If we can’t assume that, use Symbol.

        load(id){
            const loadSymbol = Symbol();
            this.loadSymbol = loadSymbol;
            this.active=id;
            this.loader=true;
            fetch(`/article/${id}`)
                .then(response=>response.json())
                .then(json=>{
                    if(this.loadSymbol != loadSymbol) return;//loadSymbol changed, so ignore
                    this.content=json;
                    this.loader=false;
                });
        }

Promise Status

Let’s look at this class:

import {Enum} from 'enumify-fork';

export default class PromiseStatus {
    constructor(promise = null) {
        this.promise = promise;
    }

    set promise(promise) {
        this.data = null;
        this.error = null;
        this.status = PromiseStatus.Status.noPromise;
        this._promise = null;
        if (promise) {
            this._promise = promise;
            this.status = PromiseStatus.Status.pending;
            promise.then(data => {
                if (this._promise === promise) {
                    this.data = data;
                    this.status = PromiseStatus.Status.resolved;
                }
            }, error => {
                if (this._promise === promise) {
                    this.error = error;
                    this.status = PromiseStatus.Status.resolved;
                }
            })
        }
    }

    get promise() {
        return this._promise;
    }
}

PromiseStatus.Status = class extends Enum {
};
PromiseStatus.Status.initEnum(['noPromise', 'pending', 'resolved', 'rejected']);

You provide promise to it, and it gives you property status, which informs you if promise is pending (data are still loading), resolved (data loaded successfully) of rejected (some error). data and error contains variables provided by resolve and reject respectively.

But what is the difference between this and standard .then().catch()? Answer is, that you can use this properties directly in vue template (for example in v-if) or computed properties, and your html will be rerendered automatically.

You can download this class from npm/yarn. Link to github repo: https://github.com/matrix0123456789/reactive-promise-status

yarn add reactive-promise-status
OR
npm install reactive-promise-status

How to use it?

<template>
    <div>
        <button @click="load(1)" :class="{active:active===1}">Show first article</button>
        <button @click="load(2)" :class="{active:active===2}">Show second article</button>
        <p v-if="content.status === Status.pending">Loading...</p>
        <p v-else-if="content.status === Status.resolved">{{content.data}}</p>
        <p v-else-if="content.status === Status.rejected">Error while loading article</p>
    </div>
</template>
<script>
import PromiseStatus from 'reactive-promise-status';

export default {
    data(){
        return {content:new PromiseStatus(), active:null, Status:PromiseStatus.Status}
    },
    methods:{
        load(id){
            this.active=id;
            this.content.promise = fetch(`/article/${id}`).then(response=>response.json());
        }
    }
};
</script>
<style>
    button.active{
        color:red;
    }
</style>

Note, that I created one object of type PromiseStatus, and in load method I changed its property promise. That is because if you set content as null and try to check content.state you will get exception. That’s why there is 4th status noPromise

Leave a comment