introduction
Vue components are nice as long as they stay small, but it's easy to end up with "fat" components if you're not careful. Components quickly become bulgy as soon as they exceed 200 lines of code, and that happens quite easily when you need to interact with an API. Luckily, business logic that doesn't strictly need to be inside a component (whose single responsibility should be rendering a template) can be extracted in various ways. Leveraging Vuex would be one. You can use mixins or the composition API, but for the purposes of this article we are interested in moving the logic to a service class that we move to a separate script. While we could import such a script directly into our component, that strategy is coupled rather tightly, which is not nice if you want to properly unit test your component. Now mocking your ES6 imports is not impossible, but it's a hassle and I don't recommend it if you can avoid it. This article suggests a strategy leveraging Vue's provide/inject mechanism to decouple service classes from components.
Define a service
For this example let's define a service with an async save method:
export default class DummyService {
async save(model) {
// do some mapping
// make call using an api client
}
}
Register the service
You can use a wrapper component, or define this in the root vue instance:
export default Vue.createApp({
provide: {
dummyService: new DummyService()
},
// other options
})
Inject the service
Here is the script part for an example vue component making use of our dummy service:
export default {
name: 'DummyComponent',
data() {
return {
isSaving: false,
model: { dummy: 'dummy' }
}
},
inject: ['dummyService'],
methods: {
async save() {
this.isSaving = true
const response = await this.dummyService.save(this.model)
// handle response
this.isSaving = false
}
}
}
Mock the service in your unit tests
Now inside our unit test for DummyComponent
we can do:
const mockDummyService = {
async save() {}
}
const wrapper = shallowMount(DummyComponent, {
provide: {
dummyService: mockDummyService
}
})
You could use mock functions inside mockDummyService
(for example those from jest) to make assertions about when and how your service is being called if you like.
But what if I need to use stuff from the Vue instance?
No worries. What you can do is setup a second Vue instance after having configured Vue which you then inject into your service. Let's adjust our example so our DummyService
uses a number of globally accessible things on the vue instance. Let's suppose:
Vue.prototype.$apiClient = new MyApiClient()
Vue.prototype.$now = () => new Date()
After any such configuration simply create a Vue instance and inject it into any services:
const secondaryVue = new Vue()
...
export default Vue.createApp({
provide: {
dummyService: new DummyService(secondaryVue)
},
// other options
})
Then for the service:
export default class DummyService {
constructor(vue) {
this.vue = vue
}
async save(model) {
model.timeStamp = this.vue.$now()
return await this.vue.$apiClient.save(model)
}
}
Through this vue
instance, you also get access to any Vue plugins like Vuex - as long as you set them up before you create the Vue instance. This way the service and vue instance also remain nicely decoupled: You can write proper unit tests for DummyService
using a mock object for the vue instance you inject.
In the introduction I mentioned some alternative approaches, so let me explain their limitations compared to this approach:
- Using Vuex or composition API: You won't have access to the vue instance, and there are no straightforward ways of injecting dependencies.
- Using mixins: obscures who owns the method or data you are calling, and can cause naming conflicts.
That's all, cheers!
Top comments (1)
[Vue warn]: Injection "injectionName" not found
Error above was driving me crazy, thanks for your article!