Best practice for loading data via Ajax


#1

Hi

I inherited an app built with Aurelia and have some issues with loading data via ajax.

There are a couple of pages which have subpages/tabs implemented as routable ViewModels.
For example: the account page has 4 subpages/tabs and default route is /account/users
/account/users
/account/notifications
/account/activity
/account/settings

In these subpages the load structure is as follows:

async attached() :Promise<void>
{
	// Note we do not await as it will block the UI rendering
	return this.loadDataAsync();
}

The this.loadDataAsync calls a local data store which either resolves from cache or issues an ajax request to backend service.

As soon as the account page loads if I keep clicking on each of the tabs over and over I get alot of loadDataAsync calls running in the background in parallel and some cause conflicts with each other.

What is the best practise of handling this type scenario?
Is this what activationStrategy is designed to address?

Thanks

Donal


#2

A q: should those tab load the data on enter like current behavior? If you change the way it loads the data, is it going to work correctly since it seems to have local cache to avoid unnecessary data fetching as well. About the attached, i don’t think it will block the render per comment, it’s a general misconception about attached. You can see it from here https://codesandbox.io/s/zlj93735y4


#3

Hi @bigopon

should those tab load the data on enter like current behavior

Ideally it would load from the cache the second time around, however I’ve been testing this on a slow machine with backend running in debug mode and quite a bit of lag to the dbs behind the backend. So what is happening is as I click through the tabs you gets the following js functions getting fired:
account.users.loadUsersAsync
account.notifications.loadNotificationsAsync
account.activity.loadActivityAsync
account.users.loadUsersAsync
account.notifications.loadNotificationsAsync
account.activity.loadActivityAsync
etc

In a really idea world, the second and subsequent times that a tab has been accessed it would know that the loadDataAsync has been already kicked off from the first time the tab was accessed and pick up from there.

If a different part of the app was accessed (/other/products) and the /account/whatever was routed to then the loadDataAsync would fire off again but this time load from cache.

Thanks

Donal


#4

I have a similar scenario in one of my apps. The parent page loads data for the first tab only. When another tab is clicked it asks the parent to get the data for that page. I am using await for all requests and am not caching. Since I am only loading 1 page at a time, the response is fast. Plus, I don’t waste time loading data for pages the user may not even visit.


#5

@rhassler

Thanks for the idea… sounds like a good workaround.

Dumb question - how do I bind to the parent page?

How did you handle this in a unit testing scenario? At the moment we can get the subpage loaded and a some basic tests ran against a mock store.

Thanks, Donal


#6

I took a look at my code and it doesn’t quite work the way I explained earlier. The parent page has a tabset control like this:

            <aubs-tabset active.bind="active">

                <aubs-tab header="Notes" on-select.call="getNotes()">
                    <notes view-model.ref="notes"></notes>
                </aubs-tab>

                <aubs-tab header="Daily Log" on-select.call="getLogs()">
                    <log-review view-model.ref="logs"></log-review>
                </aubs-tab>

                <aubs-tab header="Notes & Logs" on-select.call="getLogNotes()">
                    <log-notes view-model.ref="logNotes" startdate.bind="startdate"></log-notes>
                </aubs-tab>

                <aubs-tab header="Plan of Care" on-select.call="getPlans()">
                    <plan view-model.ref="plans"></plan>
                </aubs-tab>

                <aubs-tab header="ROI" on-select.call="getRois()">
                    <roi view-model.ref="rois"></roi>
                </aubs-tab>

            </aubs-tabset>

The view-mode.ref clauses give the parent references to the child view models. The parent view model contains this code, called by the on-select.call code above

    getCompliances() {
        this.compliances.getData(this.client);
    }

    async getContacts() {
        this.contacts.allowEdit = false;
        this.contacts = await this.contacts.getData(this.client.id);
    }

    getLogNotes() {
        this.logNotes.getData(this.client);
    }

    getNotes() {
        this.notes.getData(this.client);
    }

    getLogs() {
        this.logs.getData(this.client);
    }

    getPlans() {
        this.plans.getData(this.client);
    }

    getRois() {
        this.rois.getData(this.client);
    }

    getStatuses() {
        this.statuses.getData(this.client);
    }

This code is called by the on-select.call clauses in the view. So, when the user clicks a tab, the parent can tell the child view model to get data.

I hope this helps.


#7

@rhassler thanks for the sample - our scenario is different - we are using router view for the sub pages. Will investigate further,

thanks Donal


#8

what is your strategy for caching?

you can cache the promise (that returns from a fetch call),
so the second call to the same api will result in returning the promise (even if the data is still not here).

that way you ensure that the data is fetch only once, regardless of how many calls you made to the service. and every call gets a valid promise result - as expected.


#9

Storing the promise that kicks off the data load is the root of the problem.

One thought I had last night is that I could separate this data loading logic for the form out into its own singleton instance that is stored in the DI container, and this would contain the reference to the Promise that is returned from the store layer.


#10

Not sure about ‘best’ but if your architecture supports it, consider using AbortSignals to abort the request to the server.

Another option would be to make the VM a singleton, and only recreate the load request when the previous request is done, something like:

@singleton()
class MyComponent { 
    _isLoading = false;

    attached() {
        if (!this._isLoading) {
           this._isloading = true;  
           this.load().then(() => this._isLoading = false); // dont forget the error case here
        }
     } 
}

#11

I didn’t realise you could make your VM a singleton! This could be the simplest approach…


#12

When I said ‘service’ I meant a dedicated class that handle data fetching.
Of course it should be injected via DI, this is preferred over making the VM singleton, because you can reuse the same data over multiple VMs.


#13

@avrahamcool the data store is its own “service” which is a singleton and injected via DI, which in turn calls another service which handles the fetching.

The problem is the ViewModel forms kicking off multiple async requests against the store when you are cycling through the tabs and losing the reference to the Promise returned by each request…


#14

So just brainstorming here, but since the fetch service is separate, can it not just go ahead and cache the returned result and not care if the returned promise response is handled or not?

I am thinking that usage wise I could click through all the tabs i wanted data for, it would start fetching the data for the ones I wanted, and could then I could go back to the initial, or first tab desired, and it would either wait for the fetch to complete, or pull the freshly cached data and use it for display?

Would have to have a check if there was a request already in progress. You could control spamming fetch requests by queuing them for processing, and the just let the bindings update when there was data available (perhaps dispatch an event when the data is available). There is always the hard stop of using canDeactivate and preventing tabbing when a fetch is in progress, but might be to Draconian.

Perhaps on the tabs that have a very complex/slow response time you would put up a loading overlay to prevent any tabbing while that is happening.

I like the parent handling the request suggestion as well, but I think would still need a way to manage “That User”, you know the one, click click click click, “Hey nothing is working” :wink:


#15

The data store layer caches its results and returns those on subsequent calls.

Blocking the navigation or using canDeactivate to prevent tabbing is a no no.

I want to avoid queueing at the data store layer.

I will try the activation strategy for navigating between tabs and a loader stored in a DI singleton and see what happens with that

Thanks, Donal