Dependency Injection for React Components
Oct 13, 2016
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 componentheader.tsx
- a React component showing you a headerindex.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
- Create an interface
IApp
(this needs to be exported so we can reference it from outside):
export interface IApp extends React.Component<AppProps, AppState> {
}
- Make
App
implementIApp
:
export class App extends React.Component<AppProps, AppState> implements IApp
header.tsx
- 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> {
}
- Make
Header
implementIHeader
:
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 theHeader
class - when I ask the kernel for an
"IApp"
, give me theApp
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:
- breadboard by the wonderful people at notonthehighstreet which is designed JavaScript-first rather than Inversify's TypeScript-first.
- wire by cujojs which again is JavaScript-first.
- inverted by Phil Mander.
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!