vuex-aspect

Bind remote data of any kind to the vuex store.

Usage no npm install needed!

<script type="module">
  import vuexAspect from 'https://cdn.skypack.dev/vuex-aspect';
</script>

README

Vuex Aspect

License Demo Dependencies Version Downloads

What is Vuex Aspect?

Vuex Aspect simplifies remote data integration with the vuex store.

In short you can define Aspect instances for queries and/or subscriptions based on variables from the store. While consumed, an Aspect commits the latest result (or the error) of its query/subscription to the store.

Vuex Aspect is

  • flexible: you specify the actual queries and/or subscriptions (GraphQL / REST / WebSocket / ...)
  • reactive: aspects automatically react to variable updates (re-query / update subscription)
  • on-demand: you specify aspect consumption (component mixin / navigation mixin / API)
  • comprehensive: provides loading indications and error values
  • lightweight: no dependencies, less than 3KiB minified and gzipped

Table of Contents

Installation

npm install --save vuex-aspect

Import

import VuexAspect, {Aspect} from 'vuex-aspect';

The dist/vuex-aspect.umd.js file (within npm package) is intended for browser usage without bundling tool. It provides the global variable VuexAspect and is available via any npm-based CDN (e.g. unpkg: https://unpkg.com/vuex-aspect/dist/vuex-aspect.umd.js).

Plug into Vuex

Vuex Aspect needs to be plugged into the vuex store first:

const store = new Vuex.Store({
  plugins: [VuexAspect.install],
});

(elaborate example store instance snippet)

Usage Guide

Aspect creation

You may create as many individual aspects as needed:

const userAspect = new Aspect({
  variables({state}) { // optional function
    // get reactive variables, e.g.
    return state.userId && {id: state.userId};
  },
  async resolve(variables) {
    // run async query to fetch the new value, e.g.
    return variables && (await apollo.query({query: userQuery, variables})).data.user;
  },
});

(elaborate example aspects snippet)

(all options below)

Each Aspect instance binds a store attribute to its resolved value (and provides loading/error indication to the store). If a variables function is specified, its reactive result will trigger an update (re-invoke resolve) on each change.

On its own this aspect won't do anything thought; it needs to be registered to the store first.

Aspect registration

Each aspect can only be registered to a single vuex module.

Add an aspects attribute to your vuex module options that contains a mapping of store-keys to their respective aspect.

export default { // vuex store module options
  aspects: {
    // register userAspect to the store attributes "user", "user$loading" and "user$error"
    user: userAspect,
  },
};

(elaborate vuex module snippet)

As of now the registration of our aspects is completed. Still, the aspect does nothing. It is considered disabled as long as it has no consumer.

Aspect consumption

Aspects can have two types of consumers:

  1. Components and
  2. API access

Component consumer

Any vue component can consume some aspect simply by using its mixin property.

// some vue component options
export default {
  // consume the userAspect with this component
  mixins: [userAspect.mixin /* or userAspect.guards */],
}

If you are using vue-router, you may also use the userAspect.guards mixin to use navigation guards instead of the vue lifecycle hooks.

Note: The lifecycle hooks mixin will not defer the component being loaded. The navigation guards will defer the loading of the route until ready.

(elaborate vue component consumer example)

Now the userAspect is active whenever this component is alive. This triggers an initial query and enables the reactivity of variables. Now you may fully utilize the reactive user, user$error and user$loading values of the store.

API consumer

Aspects provide an programmatic API for consumption as well

await userAspect.grasp();
// state.user !== undefined || state.user$error !== undefined
userAspect.release();

End of consumption

When all consumers have stopped the consumption, the userAspect once again turns inactive. By default (see disable option) inactivity sets all store values to undefined again.

Aspect update

In some situations you might want to programmatically update the aspect data (e.g. remote resource change has been noticed, interval update, ...).

The aspect provides the clearCache() and fetch() methods for update interaction; those are useful for cache invalidation, interval updates, etc..

For injection of custom values use the inject(value, error) method; this is especially useful for testing. The values on the store are not supposed to be written to by anything other than the aspect instance.

API

Aspect

The Aspect({object|async function} options) constructor accepts the following options (if async function is passed, it will be used as resolve option):

  • {String} key
    • Sets the store attribute key to bind to.
    • If set, this allows usage of array notation for the aspects attribute within the store (module).
    • It is preferred to use the object notation for the aspects attribute within the store (module) instead.
  • {Object} watch
    • Additional options for vm.$watch; immediate cannot be set as it must always be true.
  • {AsyncFunction<*>} resolve({*} variables)
    • If not provided, only explicit Aspect#inject calls commit to the store.
    • The return value will be committed to the store.
  • {Function<*>} variables({Object} context)
    • context holds state, getters, rootState, rootGetters
    • The return value will be passed to the resolve function as first parameter. This function is watched by vuex while the aspect is considered to be enabled. On each change, resolve will be called with the updated result.
  • {Function} enable()
    • Called when the aspect is enabled (first consumer).
  • {Function} supply({variables, oldVariables, isInitial})
    • Called whenever the variables have changed.
    • isInitial indicates whether the call is immediately after the aspect got enabled.
    • This is most useful to update subscription-alike connections with the new variables.
  • {Function} disable()
    • Called when the aspect is disabled (no more consumer).
    • Default: function () { this.clear(); }
  • {Function} error({QueryData} queryData)
    • Called whenever the resolve function failed with an error.
    • Default: function ({reason, isDropped}) { console.error(reason); isDropped || this.clearCache(); }

All options are optional and functions (if provided) get called with the Aspect instance as this.

Aspect.prototype

  • {Vuex/Store} $store
    • The vuex Store instance.
  • {Vuex/Module} $module
    • The vuex Store-Module instance.
  • {String} id
    • The path of the value attribute within the store.
  • {Number} consumers
    • The current amount of consumers.
  • {Object} lifeMixin
    • Alias: mixin
    • A vue component mixin using lifecycle hooks for consumer registration.
    • Note: This won't defer the component creation.
  • {Object} routeMixin
    • Alias: guards
    • A vue component mixin for vue-router compatible navigation guards.
    • Note: This will defer the loading of the route.
  • {Function} context()
    • Creates a similar object to the context object that vuex actions get passed (for the module, the aspect is registered to):
      • {state, getters, commit, dispatch, rootState, rootGetters}
  • {AsyncFunction} grasp()
    • Increase consumers of the aspect by 1.
    • Resolves when store attributes are loaded.
  • {Function} release()
    • Reduce consumers of the aspect by 1.
  • {AsyncFunction<QueryData>} fetch({Boolean} clearCache)
    • clearCache == true ensures a fresh query instead of cache usage.
    • Resolves when the query has been resolved (attributes committed to the store).
  • {Function} clear()
    • Sets the attributes within the store to undefined and clears the aspect-internal cache.
  • {Function} clearCache()
    • Clear the aspect-internal cache. This enforces a query on the next grasp/fetch call.
  • {AsyncFunction<QueryData>} inject({*|Promise<*>} value, {*|Promise<*>} error)
    • Injects a value or an error to the store. Using promises enables race-condition drops similar to regular queries.
    • If error is or resolves with a null-alike value, value is looked-at instead. If either error or value (in this priority) gets rejected, the rejection reason will be used as error result.
    • Resolves when the injection has been resolved (attributes committed to the store).

QueryData

A query data object (no actual class) holds the following attributes:

  • {Number} id - The consecutive index of the query (identifying within one Aspect instance only).
  • {Boolean} hasFailed - Whether the query failed with an error.
  • {Boolean} isDropped - Whether the query got dropped (due to race-condition).
  • {Boolean} isLatest if not dropped - Whether no outstanding query has a higher index.
  • {*} variables - The variables used for the query.
  • {*} value if query succeeded - The success value.
  • {*} reason if query failed - The error reason.
  • {Boolean} isClear if true - Whether the query was issued by Aspect#clear.
  • {Boolean} isInjection if true - Whether the query was issued by Aspect#inject.

Demo

Try out the demo fiddles (mirrored within /examples/ as well):

Thanks

Thank you for considering vuex-aspect. Feel free to file issues, send merge requests or contribute in any other way!

Suggestions and feedback are highly appreciated.

Many thanks to the vue.js community for being awesome!

License

This program is licensed under MIT. It is supposed to help others but comes with no warranty of any kind.