Provide/inject services in Vue
Tags: vue
Published: 2021-04-02 16:12:15

## 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](https://vuex.vuejs.org/) would be one. You can use [mixins](https://vuejs.org/v2/guide/mixins.html) or [the composition API](https://v3.vuejs.org/guide/composition-api-introduction.html#why-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](https://jestjs.io/docs/es6-class-mocks), 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](https://v3.vuejs.org/guide/component-provide-inject.html) to decouple service classes from components.

## Define a service

For this example let's define a service with an async save method:
```js
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:
```js
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:

```js
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:

```js
const mockDummyService = {
  async save() {}
}

const wrapper = shallowMount(DummyComponent, {
  provide: {
    dummyService: mockDummyService
  }
})
```
You could use mock functions inside `mockDummyService` (for example [those from jest](https://jestjs.io/docs/mock-functions)) 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:

```js
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:

```js
const secondaryVue = new Vue()

...

export default Vue.createApp({
  provide: {
    dummyService: new DummyService(secondaryVue)
  },
  // other options
})
```

Then for the service:

```js
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!