Automatic Dependency Injection in Vue with Context Providers

  You block advertising 😢
Would you like to buy me a ☕️ instead?

I’m kind of obsessed with Dependency Injection. But for a good reason. I believe that an essential factor when it comes to building maintainable, large-scale applications is to get Dependency Injection right. I’m not saying there is only one right way; I know for a fact that there are many ways. But it is crucial to find a way that fits the overall architecture of your application.

Automatic Dependency Injection via a Context Provider.

Automatic Dependency Injection via a Context Provider

With provide/inject, we always had a powerful tool in Vue.js to develop patterns for globally injecting dependencies. But only with the improvements to Vue 3, I’m convinced that we will see widespread use of provide/inject in real-world Vue.js applications. In the last weeks, I already wrote about how we can use the Context Provider Pattern in Vue.js and utilize this technique to help us implement feature toggles. But only a few days ago, I realized that the Context Provider Pattern allows us to build the ultimate Dependency Injection system with Vue.js.

Service Container Context Provider

For the first part of our solution, we create a Service Container Context Provider. This Context Provider is responsible for setting up all of our services and making them available to our application’s components.

// src/service-container.js
import { api } from './utils/api';
import { makeCommentService } from './services/comment';
import { makeUserService } from './services/user';

export const serviceContainer = {
  commentService: makeUserService({ api }),
  userService: makeUserService({ api }),
};

In this generic service-container.js file, we create all of our service instances. Services usually contain all the business logic of our application. Typically they are responsible for fetching data and sending data to an API but depending on the type of our application, they can do anything business logic related.

// src/composables/service-container.js
import { provide, inject } from 'vue';

import { serviceContainer } from '../service-container';

export const ServiceContainerProviderSymbol = Symbol('Service container provider identifier');

export function useServiceContainerProvider() {
  provide(ServiceContainerProviderSymbol, serviceContainer);
}

export function useServiceContainerContext() {
  return inject(ServiceContainerProviderSymbol);
}

The useServiceContainerProvider() composable provides the generic Service Container to the components of our application. Now every child component of a component using useServiceContainerProvider() can useServiceContainerContext() to inject the Service Container Context.

<!-- src/components/App.vue -->
<template>
  <UserDashboard
    :user-id="currentUserId"
  />
</template>

<script>
import { useServiceContainerProvider } from '../composables/service-container';

import UserDashboard from './UserDashboard.vue';

export default {
  name: 'App',
  components: {
    UserDashboard,
  },
  setup() {
    // Provide the Service Container Context
    // to all child components of `App.vue`.
    useServiceContainerProvider();
    // ...
  },
};
</script>
// src/components/UserDashboard.vue
import { ref } from 'vue';

import { useServiceContainerContext } from '../composables/service-container';

export default {
  name: 'UserDashboard',
  props: {
    userId: {
      required: true,
      type: String,
    },
  },
  setup(props) {
    // Manually injecting the `userService`.
    let { userService } = useServiceContainerContext();
    let user = ref(null);
    let getUser = async () => {
      user.value = await userService.find(props.userId);
    };
    
    return { getUser, user };
  },
};

In the above example, we can see how we can useServiceContainerContext() to manually inject the userService from the Service Container Context. But this is tedious; what we want is automatic injection.


Do you want to learn more about advanced Vue.js techniques?

Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.


Automatic DI with Context-Aware Components

Instead of manually injecting the Service Container Context via the useServiceContainerContext() composable, we can use the Context-Aware Component Pattern to do this for us automatically.

 // src/components/UserDashboard.vue
 import { ref } from 'vue';

-import { useServiceContainerContext } from '../composables/service-container';
+import { ServiceContainerProviderSymbol } from '../composables/service-container';
+import { contextAwareComponentFactory } from '../utils/context-aware-component-factory';

-export default {
+export default contextAwareComponentFactory({
   name: 'UserDashboard',
   props: {
+    userService: {
+      context: ServiceContainerProviderSymbol,
+      required: true,
+      type: Object,
+    }
     userId: {
       required: true,
       type: String,
     },
   },
   setup(props) {
-    let { userService } = useServiceContainerContext();
     let user = ref(null);
     let getUser = async () => {
-      user.value = await userService.find(props.userId);
+      user.value = await props.userService.find(props.userId);
     };
    
     return { getUser, user };
   },
 };

Although the above approach is not necessarily shorter, changing our code to use the contextAwareComponentFactory() and getting the dependency from the component’s props has a significant advantage. The component is now explicitly telling its consumers that it requires a userService property. This means that developers immediately know that they either have to pass a userService prop manually or wrap the component in a Service Container Context. Furthermore, this also helps with testing. Instead of providing a mock implementation of userService we can pass it as a prop.

If you’re interested to learn more about how the contextAwareComponentFactory() works, you can read my article about the Context-Aware Component Pattern.

Wrapping It Up

This approach to dependency injection is very similar to how popular backend frameworks like Laravel do it. I think it is the cleanest solution to this problem without the overhead of using a separate Dependency Injection framework. But I have to say that I didn’t test this in a real-world application yet. There might be rough edges and maybe even unintended side-effects coming from wrapping Context-Aware Components with a functional component. But so far, it works great for the simple use cases I tested it with.


Do you want to learn how to build advanced Vue.js applications?

Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.



Do you enjoy reading my blog?

You can buy me a ☕️ on Ko-fi!

☕️ Support Me on Ko-fi