DEV Community

Cover image for Provide/inject services in Vue
peerhenry
peerhenry

Posted on

Provide/inject services in Vue

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
})
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
bhehar profile image
Balpreet Hehar • Edited

[Vue warn]: Injection "injectionName" not found
Error above was driving me crazy, thanks for your article!