Some devs fetch data in components and put the results in the store, others make Ajax requests inside store actions. The former might need more control over the Ajax or prefer decentralization, the latter may want components abstracted from state management.

I’m in the latter group, and I say that components shouldn’t know where or how to get data, only what data they need.

So instead of dispatch(’loadData’) , the message we really want to communicate is dispatch(‘IneedData’) .

What’s the difference?

Imagine you have 2 components on a page that leverage the same data. You may have some top level orchestration component (e.g. a page component) which is responsible for coordinating components and possibly loading all necessary data. Or you may want components to be as self-sufficient and loosely coupled as possible, if they request their own data, then it is easier to use/reuse them anywhere.

I’m in the latter group. Back to dispatch(’IneedData’) , when the store gets this request and doesn’t have the data, it must load it, but if the data is available and current, then the store does nothing. The first component to dispatch will trigger an Ajax request, the second will not.

First Pass Coding

let promise; export default {

...

actions: {

IneedData({ state, commit }) {

if (state.isLoading) {

return promise;

} else if (state.data) { // loaded

return;

}

promise = fetch("https://mydata/endpoint")

.then((rs) => rs.json())

.then((data) => commit('data', data))

.catch(...);

commit('isLoading', true);

return promise;

},

},

};

Why hold onto the promise? This means any dispatch fired while a load is going on, can still provide a promise response which will resolve when the data is available.

Exiling Promises

If we have isLoading in the module state, which is meta-data, shouldn’t we keep the promise there also? After all, store constructs are built … well, for store constructs.

A promise isn’t a serializable object, it has a purely functional interface. There’s no advantage to making it reactive in any way, and although the above code means time travel debugging won’t affect it, worth pointing out that time travel debugging won’t resurrect the Ajax request either.

…and WeakMaps?

While all this is well and good in theory, in the real world, complex data means that the dispatch might be dispatch('IneedRecordWithId', 32) .

And the module might be:

let promises = new WeakMap(); export default {

...

actions: {

IneedRecordWithId({ state, commit }, id) {

if (!state.records[id]) {

commit('initialiseRecord', id);

}

const record = state.records[id];

if (record.isLoading) {

return promises.get(record);

} else if (record.data) { // loaded

return;

}

const promise = fetch(`https://mydata/${id}/endpoint`)

.then((rs) => rs.json())

.then((data) => commit('data', data))

.catch(...)

.finally(() => promises.delete(record));

commit('isLoading', true);

promises.set(record, promise);

return promise;

},

},

};

This resembles the last module, but this time we’re dealing with state divided into records, so the module represents something like a database table. There is a records map in the state which maps each ID to a record , an Object.

Now that it is possible for any number of concurrently running Ajax requests based on specific IDs being requested, we need something slightly more complex than a global let promise .

We could use an Object/Map, but using WeakMap gives performance benefits and extra cleanup safety. If you need a refresher, from MDN:

native WeakMaps hold “weak” references to key objects, which means that they do not prevent garbage collection in case there would be no other reference to the key object. This also avoids preventing garbage collection of values in the map. Native WeakMaps can be particularly useful constructs when mapping keys to information about the key that is valuable only if the key has not been garbage collected.

WeakMaps are somewhat synonymous with privately scoped data, and this is a good example of data we want associated with our record, but not in the store state.

Exception not the Rule

This is specific solution to a specific problem, although supporting concurrent dispatches for data to the store is a pattern to be reused, relying on storage beyond the store’s state isn’t a pattern for many use cases. The best place to keep your data is in the state.

Et Voila!

Abstraction patterns and division of responsibility are generally a source of debate, if you do things a different way you may not want to change, but hopefully you enjoyed comparing an alternative approach.