Dependency injection with Vue.js

Michał Męciński
codeburst
Published in
7 min readDec 18, 2017

How dependency injection can simplify your life when working on a Vue.js application?

Dependency injection is often associated with large enterprise applications and complex frameworks. However, it’s actually much easier than it seems. In fact, if you use Vue.js, you probably already use dependency injection.

Let’s start with an example. Here’s a simple main.js file:

import Vue from 'vue'
import i18n from './i18n'
import store from './store'
import App from './components/App.vue'
new Vue( {
el: '#app',
i18n,
store,
render: h => h( App )
} );

This code creates a Vue.js application which uses a Vuex store for state management and the vue-i18n plugin to support multiple languages.

The i18n/index.js file exports an object which contains all messages in various languages:

import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use( VueI18n );export default new VueI18n( {
locale: 'en',
messages: {
...
}
} );

Similarly, store/index.js exports a typical Vuex store:

import Vue from 'vue'
import Vuex from 'vuex'
import module1 from './modules/module1'
Vue.use( Vuex );const state = {
...
};
const actions = {
...
};
export default new Vuex.Store( {
state,
actions,
modules: {
module1
}
} );

If you take a look at the fragment of code which creates the root Vue component, you will see that the i18n and store objects are passed to its constructor. Voilà, we have just used dependency injection!

These two objects can be accessed by any Vue component simply using this.$i18n and this.$store. This way, those components don’t have to import i18n/index.js and store/index.js. That’s why we say that these dependencies are “injected” into the Vue components.

Injecting custom dependencies to Vue components

Let’s suppose that the application also has a module called services/ajax.js which is just a simple wrapper for fetch:

export default function ajax( url, data = {} ) {
return fetch( url, {
method: 'POST',
body: JSON.stringify( data ),
headers: { 'Content-Type': 'application/json' }
} ).then( response => response.json() );
}

We can use the dependency injection mechanism to make this function available in every Vue component as this.$ajax.

First we need to import the function and pass it to the constructor of the root Vue component. Let’s modify main.js:

import Vue from 'vue'
import i18n from './i18n'
import ajax from './services/ajax'
import store from './store'
import App from './components/App.vue'
new Vue( {
el: '#app',
i18n,
ajax,
store,
render: h => h( App )
} );

That will not magically work on it’s own. We also need to create a very simple Vue mixin which will pass the injected dependency to all child components.

Let’s add the following code at the beginning of services/ajax.js:

import Vue from 'vue'Vue.mixin( {
beforeCreate() {
const options = this.$options;
if ( options.ajax )
this.$ajax = options.ajax;
else if ( options.parent && options.parent.$ajax )
this.$ajax = options.parent.$ajax;
}
} );

This code is essentially what both Vuex and vue-i18n do when you install them by calling Vue.use(). Let’s analyze how it works.

The beforeCreate() function of the mixin is called whenever a new instance of a Vue component is created. The this.$options object contains all custom properties passed to the constructor of the Vue component. In case of the root component it includes the ajax property, so we just assign it to this.$ajax. Otherwise, we check if it’s available in the parent of the new component. This way all components “inherit” this property from their parents, up to the root component.

You might ask why can’t we just do something like this instead:

Vue.prototype.$ajax = ajax;

This would have a similar effect: you can use this.$ajax in all Vue components to access the ajax() function.

However, the dependency injection mechanism presented above is more flexible. There can be multiple root Vue components using different implementations of the ajax() function, just like they can have separate Vuex stores and separate sets of messages. This is also useful when writing unit tests, because you can provide different mock dependencies for each test case.

Besides, modifying prototypes which are defined in third party libraries is generally not a good idea and you should avoid it.

Factory functions

If you look at the i18n/index.js file that we created above, you can see that we hard-coded the locale as ‘en’. In a real application, this information should come from somewhere.

Let’s change this module in the following way:

import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use( VueI18n );export default function makeI18n( locale ) {
return new VueI18n( {
locale,
messages: {
...
}
} );
}

Notice that it no longer exports the VueI18n object directly. Instead, it exports a factory function called makeI18n() which creates that object and allows passing the locale as an argument.

We can use the same approach to make it possible to configure the URL of the server used by the ajax() function:

export default function makeAjax( baseURL ) {
return function ajax( url, data = {} ) {
return fetch( baseURL + url, {
method: 'POST',
body: JSON.stringify( data ),
headers: { 'Content-Type': 'application/json' }
} ).then( response => response.json() );
}
}

The module now exports a factory function called makeAjax() which, when called, returns the ajax() function configured to use the correct server. It might seem a bit strange at first, but it’s an incredibly powerful and useful feature of JavaScript and other functional languages.

For this to work we need to change the main.js file in the following way:

import Vue from 'vue'
import makeI18n from './i18n'
import makeAjax from './services/ajax'
import store from './store'
import App from './components/App.vue'
const i18n = makeI18n( 'en' );
const ajax = makeAjax( 'http://example.com' );
new Vue( {
el: '#app',
i18n,
ajax,
store,
render: h => h( App )
} );

We simply import the factory functions, call them with appropriate parameters and pass the results to the root Vue component.

Injecting dependencies into Vuex store

Let’s suppose that we want to use AJAX requests and internationalized messages in the Vuex store. We can’t simply import i18n/index.js and services/ajax.js, because now they only export the factory functions. We need to somehow inject the i18n object and the ajax() function created in main.js into the Vuex store.

In order to do this, let’s change store/index.js so that it exports a factory function as well:

import Vue from 'vue'
import Vuex from 'vuex'
import makeModule1 from './modules/module1'
Vue.use( Vuex );const state = {
...
};
export default function makeStore( i18n, ajax ) {
return new Vuex.Store( {
state,
actions: makeActions( i18n, ajax ),
modules: {
module1: makeModule1( i18n, ajax )
}
} );
}
function makeActions( i18n, ajax ) {
return {
...
};
}

The dependencies are passed to the makeStore() function. That function in turn calls other factory functions to create the actions and the child modules of the store. Then it returns the created Vuex store object.

Now we must change main.js in order to create the store and pass the required dependencies to it:

import Vue from 'vue'
import makeI18n from './i18n'
import makeAjax from './services/ajax'
import makeStore from './store'
import App from './components/App.vue'
const i18n = makeI18n( 'en' );
const ajax = makeAjax( 'http://example.com' );
const store = makeStore( i18n, ajax );
new Vue( {
el: '#app',
i18n,
ajax,
store,
render: h => h( App )
} );

As you can see, dependency injection is very easy to use and it doesn’t require any complex tools or setup.

This technique is so universal that you can apply it to all kinds of modules, from simple utility functions like ajax() to very complex objects such as the Vuex store.

Using this approach, simple low level functions can be composed together to form more complex, high level functions and modules. For example, we could implement an authentication service based on the ajax() function, and then create a user management module which uses both the ajax() function and the authentication service.

I borrowed the idea of using factory functions to implement dependency injection from one of the Fun Fun Function episodes, Dependency Injection without classes by Mattias Petter Johansson. It inspired me to use this technique in my JavaScript applications and to write this article.

Benefits of dependency injection

Some of you might ask if passing dependencies to factory functions is really better than simply importing the required dependencies?

One of the things that I particularly like when using this approach is that all dependencies are clearly visible in one place. In the example above, we know that the store module depends on the i18n and ajax modules just by looking at main.js. We don’t have to look into the implementation of each module in order to figure out their dependencies.

Also the order in which the modules are initialized is immediately visible and we can easily modify it if necessary. When modules directly import other modules, the order in which they are initialized can be hard to predict and even harder to change.

This is especially important when initialization of modules has some side effects or when it needs to happen asynchronously. For example, what if the i18n module needs to fetch the dictionary of messages from the server? When modules are initialized using factory functions, we can simply postpone creating the rest of the application until the i18n module is loaded.

Code which uses dependency injection is also easier to reuse. For example, the authentication module which I mentioned earlier could be reused in another application, even if it has a different implementation of the ajax() function. Reusing modules with hard-coded dependencies is much harder.

Finally, as I already mentioned, dependency injection is helpful when using unit tests, because it makes it easy to inject mock dependencies into modules that we are testing.

Generally, I think that dependency injection is a very useful technique for developing JavaScript applications of any size. It’s definitely not as difficult as it may seem at first and it doesn’t require any special tools or frameworks.

You can notice that in the last example the locale and the URL of the server are still hard-coded in the main.js file. In the next article I will show you how to efficiently pass such information from the server to the JavaScript application.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in codeburst

Bursts of code to power through your day. Web Development articles, tutorials, and news.

Written by Michał Męciński

Author of open source projects at www.mimec.org, technical writer, co-founder of www.bulletcode.com

Responses (10)

What are your thoughts?

Dependency Injection usually has container where dependencies are instantiated and some API to extract them. The real DI is used in Angular, I cannot say I like Angular, but they did DI properly.
The examples is not DI. Just a wrapper over imported modules

--

I think this is not dependency injection. Also it is one level more abstract than factory pattern. Factory pattern provides us how to initiate an object according to some preconditions. Dependency injection, on the other hand, gives us the object directly.

--