Tomasz Swistak

|
|

Dependency Injection in React Using InversifyJS

InversifyJS is a powerful, lightweight and easy-to-use library for dependency injection in JavaScript. Unfortunately, due to the nature of React, using it as a component feature is not straightforward. This is because dependency injection in InversifyJS relies on a constructor injection, while React doesn’t want users to extend constructors of its components. However, there are methods which allow us to use inversion of control in our React code. By creating an example project, I will show you how to bypass this issue when working on projects.

EXAMPLE PROJECT 

I’ve created a simple React project in TypeScript for this article. The interesting points here are:  

  • IoC initialization 
  • NameProvider class (which will provide a name to be displayed by the React component) 
// Hello.tsx
import * as React from "react";
import { IProvider } from "./providers";

export class Hello extends React.Component {
  private readonly nameProvider: IProvider<string>;

  render() {
    return <h1>Hello {this.nameProvider.provide()}!</h1>;
  }
}
// index.tsx
import "reflect-metadata";
import * as React from "react";
import { render } from "react-dom";
import { Hello } from "./Hello";

const App = () => (
  <div>
    <Hello />
  </div>
);

render(<App />, document.getElementById("root"));
// ioc.ts
import { Container } from "inversify";
import { IProvider, NameProvider } from "./providers";

export const container = new Container();
container.bind<IProvider<string>>("nameProvider").to(NameProvider);
// providers.ts
import { injectable } from "inversify";

export interface IProvider<T> {
  provide(): T;
}

@injectable()
export class NameProvider implements IProvider<string> {
  provide() {
    return "World";
  }
}

Listing 1IoC initialization and NameProvider

It’s a typical usage of InversifyJS, creating a new IoC container to which we bind a class under a given identifier (in this case a string, but it can also be a Symbol or a type). This is a transient scope declaration, which means that for every injection, it will create a new instance of the class. There are also singleton scope (always the same instance for every injection) and request scope (the same instance for one call of container.get). 

Every class which can be injected through Inversify has to be decorated with injectable, usually by using an inject decorator inside the constructor or on properties. However, this is not possible with React, so we have to do it differently. 

The rest of the project is a simple React app. Hello is a component that uses a provider to display a name in a header. In the index we are rendering it in DOM. The code won’t work until we inject our NameProvider instance. 

USING INVERSIFY-INJECT-DECORATORS 

This is the recommended way of using Inversify when we can’t do a constructor injection. The library provides four additional decorators to use in projects: lazyInjectlazyInjectNamedlazyInjectTaggedlazyMultiInject. As the name suggests, they provide a lazily evaluated injection, which means the dependency isn’t provided, as is normal, while the object is initialized. Instead it is provided during its first usage and is then cached for later. Caching can be turned off by providing a proper Boolean value during initialization. 

To do this, we first execute the getDecorators function, which will return the decorators mentioned above. In this case, we will only use lazyInject, since we don’t use names or tags and we don’t bind multiple classes under one identifier to use multi-injection. We then just decorate our nameProvider property with lazyInject

// Hello.tsx
import * as React from "react";
import { lazyInject } from "./ioc";
import { IProvider } from "./providers";

export class Hello extends React.Component {
  @lazyInject("nameProvider") private readonly nameProvider: IProvider<string>;

  render() {
    return <h1>Hello {this.nameProvider.provide()}!</h1>;
  }
}
// ioc.ts
import { Container } from "inversify";
import getDecorators from "inversify-inject-decorators";
import { IProvider, NameProvider } from "./providers";

const container = new Container();
container.bind<IProvider<string>>("nameProvider").to(NameProvider);

const { lazyInject } = getDecorators(container);

export { lazyInject };

Listing 2lazyInject export and component with the injected provider

It’s fairly simple to use in most React and InversifyJS projects. Unfortunately, it’s quite common to get circular dependencies using this method, especially while having container modules in separate files and later combining them into one container, in something similar to an ioc.ts file. This can be avoided by dividing module loading and exporting lazyInject into separate files. You can see the whole example here

USING INVERSIFY-REACT 

inversify-react is a library which carries out dependency injection in a slightly different way. For our usage, we get a React component Provider which holds the IoC container and passes it down the React tree. We also get four decorators – provideprovide.singletonprovide.transient and resolve. In our example, we will need only resolve to get the dependency and Provider to pass it to the container. resolve works in a similar way to lazyInject in the previous method, but it doesn’t require any initialization. 

To use it, we simply put our app inside the Provider component to which we are passing the IoC container. In Hello, we then decorate nameProvider with resolve and provide it with a proper identifier. ioc.ts should remain unchanged (in contrast to the previous method). You can check all changes here: 

// Hello.tsx
import * as React from "react";
import { resolve } from "inversify-react";
import { IProvider } from "./providers";

export class Hello extends React.Component {
  @resolve("nameProvider") private readonly nameProvider: IProvider<string>;

  render() {
    return <h1>Hello {this.nameProvider.provide()}!</h1>;
  }
}
// index.tsx
import "reflect-metadata";
import * as React from "react";
import { render } from "react-dom";
import { Provider } from "inversify-react";
import { Hello } from "./Hello";
import { container } from "./ioc";

const App = () => (
  <Provider container={container}>
    <div>
      <Hello />
    </div>
  </Provider>
);

render(<App />, document.getElementById("root"));

Listing 3 Usage of Provider and resolve

This easy method means we don’t need any changes within IoC, we are just passing a container down the React tree and getting dependencies using a proper decorator. However, the library is currently in the early stage of development and the API may even change at any time without warning. Another downside is the fact that we only get the simplest case of dependency injection. It will cover most cases, but I’ve needed multi-injection in some projects, which I could not do with this library. You can see the whole example here.

USING REACT-INVERSIFY 

The name is similar to the previous library, but this one has an entirely different approach. We have both a Provider component, which is similar to that in inversify-react, and a higher order connect component, which injects dependencies to props. 

It is trickier to use than previous methods as we don’t use decorators inside components. We need to create a class where we inject dependencies in a typical way, using inject. We then use connect HOC to provide an instance of this class to the properties of our component, as such:

// Hello.tsx
import * as React from "react";
import { inject, injectable } from "inversify";
import { connect } from "react-inversify";
import { IProvider } from "./providers";

type Props = {
  nameProvider: IProvider<string>;
};

@injectable()
class Dependencies {
  constructor(
    @inject("nameProvider") public readonly nameProvider: IProvider<string>
  ) {}
}

@connect(
  Dependencies,
  deps => ({
    nameProvider: deps.nameProvider
  })
)
export class Hello extends React.Component<Props> {
  render() {
    return <h1>Hello {this.props.nameProvider.provide()}!</h1>;
  }
}
// index.tsx
import "reflect-metadata";
import * as React from "react";
import { render } from "react-dom";
import { Provider } from "react-inversify";
import { Hello } from "./Hello";
import { container } from "./ioc";

const App = () => (
  <Provider container={container}>
    <div>
      <Hello />
    </div>
  </Provider>
);

render(<App />, document.getElementById("root"));

Listing 4Usage of Provider and connect

Usage is a bit more complicated, as we need to create an additional class and wrap our component with HOC. In the example, I’ve used it as a decorator but you can also use it in a traditional way. Generally, this method is most in line with React, since objects are passed as properties, not instantiated inside the component. Another advantage is the full usage of InversifyJS, meaning we don’t need to depend on what the library offers—but it does involve writing a lot more code than other solutions. You can see the whole example here

SUMMARY 

We therefore have three ways of using InversifyJS in React projects. Each has advantages and disadvantages, meaning that there is no perfect solution, but I hope this makes it a little easier for you to choose the library that best suits your project.

This post was also published on ITNEXT.