README
Vuex Aspect
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)
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:
- Components and
- 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 betrue
.
- Additional options for vm.$watch;
{AsyncFunction<*>} resolve({*} variables)
- If not provided, only explicit
Aspect#inject
calls commit to the store. - The return value will be committed to the store.
- If not provided, only explicit
{Function<*>} variables({Object} context)
context
holdsstate, 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(); }
- Called whenever the
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.
- Alias:
{Object} routeMixin
- Alias:
guards
- A vue component mixin for
vue-router
compatible navigation guards. - Note: This will defer the loading of the route.
- Alias:
{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}
- Creates a similar object to the context object that vuex actions get passed (for the module, the aspect is registered to):
{AsyncFunction} grasp()
- Increase consumers of the aspect by
1
. - Resolves when store attributes are loaded.
- Increase consumers of the aspect by
{Function} release()
- Reduce consumers of the aspect by
1
.
- Reduce consumers of the aspect by
{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.
- Sets the attributes within the store to
{Function} clearCache()
- Clear the aspect-internal cache. This enforces a query on the next
grasp
/fetch
call.
- Clear the aspect-internal cache. This enforces a query on the next
{AsyncFunction<QueryData>} inject({*|Promise<*>} value, {*|Promise<*>} error)
- Injects a
value
or anerror
to the store. Using promises enables race-condition drops similar to regular queries. - If
error
is or resolves with anull
-alike value,value
is looked-at instead. If eithererror
orvalue
(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).
- Injects a
QueryData
A query data object (no actual class) holds the following attributes:
{Number} id
- The consecutive index of the query (identifying within oneAspect
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 byAspect#clear
.{Boolean} isInjection
if true - Whether the query was issued byAspect#inject
.
Demo
Try out the demo fiddles (mirrored within /examples/ as well):
- simple example
- persisted example (using
vuex-persistedstate
) - subscription example
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.