Our web application is in the process of being iteratively migrated from Backbone to Vue (for UI) and Vuex (for state management). As we write more and more code in these new frameworks, patterns begin to emerge. In Vue components, there is a mechanism for extracting common horizontal patterns: mixins. We've used component mixins in several places and they have helped to simplify our code. However, there is no comparable method of "mixing in" functionality to Vuex stores and modules. What follows is an explanation, justification and proposal for Vuex mixins.
Vue Component Mixins
To give some context to the conversation, let's first look at the Vue mixin API for components as it exists. The easiest way to do this is a concrete example. Imagine we have two, unrelated components: a blog post description and a thumbnail.
export const Description = {
template: '#description',
props: {
text: {
type: String,
required: true,
},
},
};
export const Thumbnail = {
template: '#thumbnail',
props: {
srcUrl: {
type: String,
required: true,
},
},
};
At some point, we discover that these two components actually share some functionality: they both have the ability to be expanded. That might mean something slightly different in each component, but the bulk of the code is the same.
export const Description = {
template: '#description',
props: {
text: {
type: String,
required: true,
},
},
data() {
return {
expanded: false,
};
},
methods: {
toggleExpanded() {
this.expanded = !this.expanded;
},
},
};
export const Thumbnail = {
template: '#thumbnail',
props: {
srcUrl: {
type: String,
required: true,
},
},
data() {
return {
expanded: false,
};
},
methods: {
toggleExpanded() {
this.expanded = !this.expanded;
},
},
};
A pattern like component extension, which resembles traditional inheritance, doesn't work in this situation. A description and thumbnail are probably not related in an Object Oriented hierarchy. Instead, we can take that shared functionality and create a mixin, which is a common pattern for multiple inheritance. Vue has a built in API for component mixins.
const expandable = {
data() {
return {
expanded: false,
};
},
methods: {
toggleExpanded() {
this.expanded = !this.expanded;
},
},
};
export const Description = {
template: '#description',
mixins: [expandable],
props: {
text: {
type: String,
required: true,
},
},
};
export const Thumbnail = {
template: '#thumbnail',
mixins: [expandable],
props: {
srcUrl: {
type: String,
required: true,
},
},
};
This simplifies and reduces redundancy in our code without creating a hierarchical relationship between the two components. Hopefully looking at this example starts to open up your mind to the possibilities of this pattern.
Shared Functionality in Vuex Stores
In our application, each page is a mini-application with its own Vuex Store for managing the application's state. Many of these stores, as one might imagine, share some functionality.
For instance, many stores need the ability to update the value of top-level keys via a mutation. Here is an example of what the store for a blog post might look like:
import Vue from 'vue';
import Vuex from 'vuex';
const BASE_URL = 'http://myblog.com/post';
const options = {
state: {
description: '',
imageUrl: '',
text: '',
},
mutations: {
update(state, payload) {
if (payload!) {
return;
}
Object.keys(payload).forEach((key) => {
if (Object.hasOwnProperty(key)) {
state[key] = payload[key];
} else {
Vue.set(state, key, payload[key]);
}
});
},
},
actions: {
load({ commit }, id) {
fetch(`${BASE_URL}/${id}`)
.then(response => response.json())
.then(json => commit('update', json));
},
},
};
export default new Vuex.Store(options);
In this store the state
and actions
are pretty specific to this domain of the application. However, the update
mutation is rather generic. We could easily imagine many stores using this mutation, so let's attempt to extract that logic in some way.
First Attempt: Mixin as Function
Our first attempt to create mixin-like multiple inheritance was to create a function that added the mutation to the base options of a store. This function could then wrap any store to add the ability to mutate top level keys.
import Vue from 'vue';
import Vuex from 'vuex';
const BASE_URL = 'http://myblog.com/post';
const addUpdateMixin = ({ mutations = {}, ...rest }) => {
return {
mutations: {
update(state, payload) {
if (!payload) {
return;
}
Object.keys(payload).forEach((key) => {
if (Object.hasOwnProperty(key)) {
state[key] = payload[key];
} else {
Vue.set(state, key, payload[key]);
}
});
},
...mutations,
},
...rest,
};
};
const options = {
state: {
description: '',
imageUrl: '',
text: '',
},
actions: {
load({ commit }, id) {
fetch(`${BASE_URL}/${id}`)
.then(response => response.json())
.then(json => commit('update', json));
},
},
};
const mixedInOptions = addUpdateMixin(options);
export default new Vuex.Store(mixedInOptions);
Now we can use this function in any store and greatly simplify the internal logic. A downside here is that creating these mixin functions is a bit cumbersome; perhaps we can improve upon it.
Second Attempt: Mixin Utility
What if we could define a mixin in much the same way we define a store? Instead of defining it as a function, wouldn't it be nice if we could instead define it as an object of the same shape as a store?
We think so. This makes defining a store mixin similar to defining a component mixin in Vue.
In order to do this, we would need to define a generic "mixin" function that combines mixins with store options.
import Vue from 'vue';
import Vuex from 'vuex';
const BASE_URL = 'http://myblog.com/post';
const STORE_KEYS = ['state', 'actions', 'getters', 'mutations', 'modules'];
const mixin = (options, mixins) => (
mixins.reduce((acc, mixin) => {
const mixed = {};
STORE_KEYS.forEach((k) => {
mixed[k] = {
...(mixin[k] || {}),
...(acc[k] || {}),
};
});
return mixed;
}, { ...options });
);
const updatable = {
mutations: {
update(state, payload) {
if (payload!) {
return;
}
Object.keys(payload).forEach((key) => {
if (Object.hasOwnProperty(key)) {
state[key] = payload[key];
} else {
Vue.set(state, key, payload[key]);
}
});
},
},
};
const options = {
state: {
description: '',
imageUrl: '',
text: '',
},
actions: {
load({ commit }, id) {
fetch(`${BASE_URL}/${id}`)
.then(response => response.json())
.then(json => commit('update', json));
},
},
};
const mixedInOptions = mixin(options, [updatable]);
export default new Vuex.Store(mixedInOptions);
This method also allows us to easily add multiple mixins to the same store, while always giving the store the ability to override any property within the mixin. As an in-house solution, this works pretty well. The only thing that might make this better is if it were part of the Vuex API.
Third Attempt: Existing Vuex APIs
After pouring over the Vuex docs, it looked to us like there were two candidates for existing APIs to do some sort of mixin-like composition: non-namespaced modules or plugins. Let's take a look at the viability of these options.
Non-Namespaced Modules as Mixins
The primary use of modules in Vuex stores is to separate application state into smaller, more manageable chunks by logical domain. Typically, modules are "namespaced" such that their state in addition to all actions, getters, and mutations (or even submodules) are found under the module's name (i.e. 'post/update').
If, however, the module is not namespaced, one could conceivably merge actions, getters and mutations into the parent store's namespace.
Let's see how that might look for our updatable
mixin.
import Vue from 'vue';
import Vuex from 'vuex';
const BASE_URL = 'http://myblog.com/post';
const updatable = {
mutations: {
update(state, payload) {
if (payload!) {
return;
}
Object.keys(payload).forEach((key) => {
if (Object.hasOwnProperty(key)) {
state[key] = payload[key];
} else {
Vue.set(state, key, payload[key]);
}
});
},
},
};
const options = {
state: {
description: '',
imageUrl: '',
text: '',
},
modules: {
updatable,
},
actions: {
load({ commit }, id) {
fetch(`${BASE_URL}/${id}`)
.then(response => response.json())
.then(json => commit('update', json));
},
},
};
export default new Vuex.Store(options);
This looks pretty good…but it won't work. Even when modules are not "namespaced" using the namespaced
option, their state is always namespaced (in this case state.updatable
). Furthermore, the state
parameter on the module's mutation functions is limited to that module's sub-state. You cannot get access to the root state within a module mutation. However, getters and actions do get access to root state, albeit through a separate and explicit parameter.
Bottom line: this approach can work for some mixin type things if they either 1. don't need access to root state at all and can work entirely through sub-state or 2. don't use mutations at all.
Ultimately, I don't think using modules in this way is a good idea. It muddies the waters as to what the purpose of modules are in the first place and has too many caveats to feel right.
Plugins as Mixins
Plugins have an advantage over modules in that they are designed to extend the functionality of a store much like a mixin. Also, they get access to an instantiated store's API rather than modifying a store's options. This is nice because there is a much more explicit relationship between store and plugin where neither needs to be particularly aware of the other's internals.
There are some downsides that ultimately make plugins untenable for our purposes. While a plugin can "dispatch" an action or "commit" a mutation, it cannot define actions or mutations. This makes it impossible to even attempt to implement something like our updatable
mixin using a plugin. So, sadly, this will not do either.
Next Attempt: A Proposal for a Vuex Mixin API
Now that we have built the case for mixins and exhausted possible implementations using existing APIs, it is time we do what we said we would in the title of this article: propose a new API to solve this problem.
The API would both be very simple and very familiar to those who have used mixins already in Vue components. Here is what it might look like for our updatable
mixin:
import Vue from 'vue';
import Vuex from 'vuex';
const BASE_URL = 'http://myblog.com/post';
const updatable = {
mutations: {
update(state, payload) {
if (payload!) {
return;
}
Object.keys(payload).forEach((key) => {
if (Object.hasOwnProperty(key)) {
state[key] = payload[key];
} else {
Vue.set(state, key, payload[key]);
}
});
},
},
};
const options = {
state: {
description: '',
imageUrl: '',
text: '',
},
mixins: [updatable],
actions: {
load({ commit }, id) {
fetch(`${BASE_URL}/${id}`)
.then(response => response.json())
.then(json => commit('update', json));
},
},
};
export default new Vuex.Store(options);
Simple as that. Under the hood, it would act much like our mixin
reducer did in a previous example: merging in state, actions, getters, mutations, and modules while allowing the store to override any property.
The possibilities here are pretty cool. For example, in a future post, we'll show you how we use store mixins to persist application state to localstorage.
If you have any ideas about this (i.e. why we're wrong, other ways to use existing APIs, other ways of composing functionality, ideas for store mixins, etc.) we'd love to hear from you!