Using InversifyJS in NuxtJS

Unlike Angular, Vue.js doesn't have a built-in dependency injection framework and neither has NuxtJS. There's a way to inject members into components with Vue.js and NuxtJS plugins but that's just a small subset of what a real dependency injection framework can do. Of course, nothing is stopping you from using one in your Vue.js or NuxtJS application. InversifyJS is a popular choice in the JavaScript ecosystem.

The installation procedure will be the same as in any other JavaScript application:

  • Install the required NPM packages:

    npm i inversify reflect-metadata
    
  • Update the tsconfig.json file by setting the emitDecoratorMetadata and adding reflect-metadata to the types array (or setting typeRoots instead):

    {
      "compilerOptions": {
        "target": "es2018",
        "module": "esnext",
        "moduleResolution": "node",
        "lib": ["esnext", "esnext.asynciterable", "dom"],
        "esModuleInterop": true,
        "allowJs": true,
        "sourceMap": true,
        "strict": true,
        "noEmit": true,
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "baseUrl": ".",
        "paths": {
          "~/*": ["./*"],
          "@/*": ["./*"]
        },
        "typeRoots": ["node_modules/@types"]
      },
      "exclude": ["node_modules", ".nuxt", "dist"]
    }
    

For each dependency you will have to define a type (or use an existing one as I do below for the NuxtAxiosInstance):

export interface GithubRepository {
  getOrgRepos(org: string): Promise<Repo[]>;
}

And implement it, of course:

import { inject, injectable } from 'inversify';
import { NuxtAxiosInstance } from '@nuxtjs/axios';

@injectable()
export class AxiosGithubRepository implements GithubRepository {
  constructor(
    @inject(SYMBOLS.NuxtAxiosInstance) private axios: NuxtAxiosInstance
  ) {}

  async getOrgRepos(org: string): Promise<Repo[]> {
    return await this.axios.$get<Repo[]>(
      `https://api.github.com/orgs/${org}/repos`
    );
  }
}

The implementations must be decorated with the @injectable decorator.

Constructor injection can be used for injecting dependencies. Each parameter must be decorated with the @inject decorator. Its parameter is a type identifier that must be defined for each dependency:

export const SYMBOLS = {
  GithubRepository: Symbol('GithubRepository'),
  NuxtAxiosInstance: Symbol('NuxtAxiosInstance'),
};

The same identifiers are used when configuring the dependency injection container:

import 'reflect-metadata';
import { Container } from 'inversify';

export const container = new Container();
container
  .bind<GithubRepository>(SYMBOLS.GithubRepository)
  .to(AxiosGithubRepository)
  .inSingletonScope();

I used singleton scope in the snippet above but other options are supported as well.

The import for reflect-metadata is really important. It must be done exactly once in your application. The file with container configuration seems a good place for it. If you forget to do it, the application will fail at runtime with the following error:

Reflect.hasOwnMetadata is not a function

You might have noticed that I haven't configured the binding for the NuxtAxiosInstance. That's because its instance can't simply be created. The NuxtJS Axios module takes care of its initialization and makes it available in the NuxtJS context. Such dependencies will need to be bound in a NuxtJS plugin which ensures that the code is run before the Vue.js application is started (and after the modules are initialized):

import { Context } from '@nuxt/types';
import { NuxtAxiosInstance } from '@nuxtjs/axios';

export default ({ $axios }: Context) => {
  container
    .bind<NuxtAxiosInstance>(SYMBOLS.NuxtAxiosInstance)
    .toConstantValue($axios);
};

As you can see, I get the instance from the NuxtJs context and bind it as a constant value for its type.

The plugin must be registered in nuxt.config.js to be run:

export default {
  // ...
  plugins: ['plugins/bindInversifyDependencies.ts'],
  // ...
};

The constructor injection will only work when InversifyJS is creating an instance with the dependency. If that's not the case, lazy property injection can be used instead. This requires some additional setup:

  • NPM package installation:

    npm i inversify-inject-decorators
    
  • Initialization of the @lazyInject decorator (best done in the same file as the InversifyJS container configuration):

    export const { lazyInject } = getDecorators(container);
    

To take advantage of that in components (and NuxtJS pages), the class-style syntax must be used:

@Component
export default class IndexPage extends Vue {
  repos: Repo[] = [];
  org: string = '';

  @lazyInject(SYMBOLS.GithubRepository)
  private githubRepository!: GithubRepository;

  async refresh(): Promise<void> {
    this.repos = await this.githubRepository.getOrgRepos(this.org);
  }
}

Unfortunately, sometimes even this isn't possible. For example, the NuxtJS asyncData method is invoked before the component is initialized. In this case, the only option is to request the dependency instance directly from the container:

@Component({
  asyncData: async (_context: Context) => {
    const githubRepository = container.get<GithubRepository>(
      SYMBOLS.GithubRepository
    );
    const repos = await githubRepository.getOrgRepos('damirscorner');
    return { repos };
  },
})
export default class IndexPage extends Vue {}

This covers all the different use cases I've encountered in NuxtJS so far. However, in development mode the application will still fail with the following error when using SSR (server-side rendering):

Cannot read property 'constructor' of null

To resolve it, the following configuration must be added to the nuxt.config.js file:

export default {
  // ...
  render: {
    bundleRenderer: {
      runInNewContext: false,
    },
  },
  // ...
};

Source code for a working NuxtJS sample application using all of the above is available in my GitHub repository.

Since Vue.js and NuxtJS don't have a built-in dependency injection framework, InversifyJS can be added to your project if you would like to take advantage of this pattern. To use it fully, you will need some understanding of the NuxtJS internals. This post should help you set up everything initially and learn the patterns to use in different scenarios.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License