James Monger Computers, cars, cynicism

Dependency Injection for React Components

Dependency Injection, often written as DI, is a great way to ensure your code is as reusable and as clean as possible. The main benefit, with the help of an “injection container”, is that you can switch modules out for other modules easily and in a single place.

This tutorial assumes that you’re familiar with setting up a React project, and ideally you’ll have read through some of the InversifyJS documentation so that you’re familiar with the syntax.

Why?

People practice Dependency Injection (or, more generically, inversion of control) for a number of reasons:

  • It can act as a software design aid, forcing you to keep your code modules discrete and independent.
  • It allows modules to be easily replaced for other modules which do the same thing but in a different way.
  • It helps prevent assumptions between modules - they only care that other modules do an action and they don’t care about how they do it or what they need to do it.

Generic example

An easy way to understand Dependency Injection is through looking at an example. So let’s see a quick example to do with a garden. (Because the end project will be written in TypeScript, the examples along the way will also be written in TypeScript.)

The first example will be without Dependency Injection - the Gardener class will instantiate the objects that it needs.

class Plant {
    public name: string;

    constructor (name: string) {
        this.name = name;
    }
}

class WateringCan {
    private material: string;

    constructor (material: string) {
        this.material = material;
    }

    public water (plant: Plant): void {
        // do your stuff here
    }
}

class Gardener {
    private plant: Plant;
    private wateringCan: WateringCan;

    constructor () {
        this.plant = new Plant("Daffodil");
        this.wateringCan = new WateringCan("Steel");
    }

    public waterPlant(): void {
        this.wateringCan.water(this.plant);
    }
}

Because the Gardener class creates its own concrete objects, it will be hard to change those - they will need to be changed in multiple places. We won’t be able to change the project to use a new type of WateringCan without also changing Gardener and other classes. This is an example of tight coupling, and is something that we want to avoid in our code.

Therefore, so that our code isn’t as tightly coupled, we should interface out the dependencies and reference these interfaces instead. This is the Dependency Inversion Principle, which is the D in SOLID.

interface IPlant {
    name: string;
}

class Plant implements IPlant {
    // do your stuff here
}

interface IWateringCan {
    material: string;
    water (plant: IPlant): void;
}

class WateringCan implements IWateringCan {
    // do your stuff here
}

class Gardener {
    private plant: IPlant;
    private wateringCan: IWateringCan;

    public waterPlant(): void {
        this.wateringCan.water(this.plant);
    }
}

As you can see, Gardener, which is dependent on IPlant and IWateringCan has those injected in through its constructor so that it can do its stuff. This way, we only need to change the thing that makes the Gardener to use different IPlant and IWateringCan objects - we don’t need to make changes all the way through the project.

How can we do this automatically so that we don’t need to manually create a Gardener class with the dependencies? We can use an IoC container such as Inversify (the one I told you to read up on at the start of this post!) to do all the instantiation that we need.

kernel.bind<IPlant>("IPlant").toConstantValue(Plant);
kernel.bind<IWateringCan>("IWateringCan").toConstantValue(WateringCan);

All you need to do after updating your bindings is add the inject annotation to the properties:

class Gardener {
    @inject("IPlant")
    private plant: IPlant;

    @inject("IWateringCan")
    private wateringCan: IWateringCan;
}

This way, if you create a new class which implements IWateringCan, such as ElectricWateringCanWhichMakesItAllEasierForTheGardener, you only need to change it in your container and nowhere else - one change and you can roll the new implementation out across your whole project!

How do we do it in React?

Setting up your container for a fake project to do with gardening is one thing, but what about in a real world application? Sometimes you will see people injecting helper classes (such as IRandomNumberGenerator), providers (such as IImageUrlProvider) or data classes (such as ICarRepository). But what about the components which are rendered on the page - can they be injected?

In this example, I’m going to have a small React app. It will, however, give you enough information to set this up on larger applications.

Setting up

You’ll want to have the following structure:

  • public
    • index.html - the web page which your React app is displayed on
  • app.tsx - the top-level React component
  • header.tsx - a React component showing you a header
  • index.ts - the entry point for the application

(There will be a couple of other files you’ll need, but these are files such as package.json which will be generic across most apps.)

Initial app

The initial app will be set up without any dependency injection. It will create anything it needs instantiated, and it will rely on concrete implementations.

index.html

Make sure you point ./bundle.js to your bundled app, or set up another module loading solution such as RequireJS. I chose to use webpack when doing the examples.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>React Dependency Injection</title>
</head>
<body>
    <div id="react-root"></div>

    <script src="./bundle.js" type="text/javascript"></script>
</body>
</html>

app.tsx

Notice how this injects the Header component from header.tsx. Imagine if you had to change this to be a different implementation, but you weren’t able to actually modify the Header class itself. You’d have to change everywhere that references Header.

import * as React from "react";

import { Header } from "./header";

export interface AppProps { }

export interface AppState { }

export class App extends React.Component<AppProps, AppState> {
    public render(): React.ReactElement<{}> {
        return <section>
            <Header title="hi there" />
        </section>;
    }
}

header.tsx

This component is pretty simple. It just has a title in its properties, and displays this in an h1 element.

import * as React from "react";

export interface HeaderProps {
    title: string;
}

export interface HeaderState { }

export class Header extends React.Component<HeaderProps, HeaderState> {
    public render(): React.ReactElement<{}> {
        return <h1>{ this.props.title }</h1>;
    }
}

index.ts

This is the entry point - it loads the App component and displays it in #react-root.

import * as React from "react";
import * as ReactDOM from "react-dom";

import { App } from "./app";

ReactDOM.render(React.createElement(App, null), document.getElementById("react-root"));

Inverting the control

To make this code as clean as possible, we need to make it so that it depends on injected interfaces rather than grabbing its own concrete implementations. I am going to use Inversify which is a container that injects dependencies through the constructor (aptly named “constructor injection”). Some other methods are “property injection” and “setter injection”, but I will leave these as later reading. I like constructor injection because the class cannot be created without them and they are clear dependencies.

Setting up interfaces

We need to set up some interfaces which have the behaviour we want interfaced out on them. We will be abstracting out the App and Header components into IApp and IHeader interfaces. The components have no behaviour of their own, so we will make the interfaces extend React.Component as well.

app.tsx
  1. Create an interface IApp (this needs to be exported so we can reference it from outside):
export interface IApp extends React.Component<AppProps, AppState> {

}
  1. Make App implement IApp:
export class App extends React.Component<AppProps, AppState> implements IApp
header.tsx
  1. Create an interface IHeader (this also needs to be exported so we can reference it from outside):
export interface IHeader extends React.Component<HeaderProps, HeaderState> {

}
  1. Make Header implement IHeader:
export class Header extends React.Component<HeaderProps, HeaderState> implements IHeader

Setting up bindings

Inversify works by having “bindings”: we “bind” a key (we’ll be using a string) to a certain object, and then whenever we ask for one of those keys to be injected, we are given the object which is bound to it. With Inversify, we can do a number of different binding types. We want to use toConstantValue because React works by rendering the actual class type rather than an instance of the class (by default, Inversify will inject an instance of the class for us).

Create a file inversify.config.ts, then create a Kernel inside it and export is as the default export:

import "reflect-metadata";
import { Kernel } from "inversify";
import getDecorators from "inversify-inject-decorators";

let kernel = new Kernel();
let decorators = getDecorators(kernel);
let inject = decorators.lazyInject;

export {
    kernel,
    inject
};

We need to use getDecorators in the example above to create a property injector - constructor injection doesn’t play nicely with React.

Now we want to set up our bindings for the IApp and IHeader components:

import { IHeader, Header } from "./header";
kernel.bind("IHeader")
    .toConstantValue(Header);

import { IApp, App } from "./app";
kernel.bind("IApp")
    .toConstantValue(App);

What this says is:

  • when I ask the kernel for an "IHeader", give me the Header class
  • when I ask the kernel for an "IApp", give me the App class

Using the bindings

We need to make it so that IHeader is injected into App, and that we use the kernel to resolve an IApp in index.ts.

app.tsx

We need to import the inject annotation from our Inversify configuration:

import { inject } from "./inversify.config";

Import the IHeader interface and delete the concrete Header import:

import { IHeader } from "./header";

We need to store header as a variable in the App class so that it can be referenced from our render method:

@inject("IHeader")
private header: new () => IHeader;

The new () => IHeader; type looks a bit weird, but it’s not as bad as it looks. It means a type that, when instantiated, returns an IHeader - so just a reference to a class implementing IHeader. As you can see, it’s marked with @inject so Inversify knows to inject it.

Lastly, we need to use this in our render method. Rather than using Header, use this.header.

<this.header title="hi there" />

That’s it for app.tsx, it’s now set up to receive an IHeader reference from our IoC container.

index.ts

We need to make it so that index.ts uses the kernel to resolve App. That way, it will automatically resolve the dependencies all the way down.

Import the kernel from inversify.config.ts, and import the IApp interface:

import { kernel } from "./inversify.config";
import { IApp } from "./app";

Get the concrete class reference for IApp from the kernel:

let App = kernel.get<new () => IApp>("IApp");

Further reading

If you want to see the source code for my example, you can take a look at github: jameskmonger/react-injection.

My solution should look pretty much the same as yours, but there are a few things I haven’t mentioned.

  • tsconfig.json: This will be set up to your requirements, feel free to take a look at mine which builds an ES6 CommonJS project. If you choose not to use TypeScript (you should, really!) you won’t have this file.
  • webpack.config.ts: As explained earlier, I chose to use webpack to bundle my project. Some alternatives are rollup, browserify, or even hand-rolling it if you’re feeling especially masochistic. It’s your choice and shouldn’t really have any impact on the tutorial.
  • text.tsx: I created another level of component so that you can see how injection can go down multiple levels in the stack.

Besides Inversify, there are a number of other IoC containers:

If you’re unsure about whether you need to use an IoC container (why not just pass App the dependencies it needs? Why get something else to?); if your dependency chain will never go so deep that manually resolving them all is a pain then great - you probably won’t need an IoC container. However the scalability that an IoC container gives you (simply make a new class and add the inject annotation, in the case of InversifyJS) is great in my opinion. Read a great answer to the question “Why do I need an IoC container?” on Stack Overflow for more information on that point.

And of course, it wouldn’t be an informative blog post on dependency injection without a link to the great article by Martin Fowler, so go and read that as soon as you can if you haven’t already (if you have, you’ve still probably got stuff to gain from reading it another five or six times).

Happy coding!