Merrick Christensen's Avatar
I have been impressed with the urgency of doing. Knowing is not enough; we must apply. Being willing is not enough; we must do.Leonardo Davinci

React + Angular DI

2014-12-20

Update June 7, 2018 - Hall of Shame

This article is a Hall of Shamer™ because it was a terrible (but fun) idea.

I have been deliberating on the topic of writing testable React code without singletons and service locator “injection” for some time. My last article proposed a very straightforward approach and discussed why I am chasing these goals. Well, I am very happy to say that I feel I’ve settled on an approach and an unlikely one at that. I believe the following approach offers the following:

  1. Code isn’t “coupled” to the dependency injector, it can be run without it using vanilla constructor injection.
  2. Code is testable without module system level mocking.

Link to the discussed project is here.

di.js

No front-end web framework has pioneered testability like Angular.js. At the forefront of this effort is the dependency injector. Angular 2 has a little known, fantastic, project in the works called di.js.

di.js gives us the testability of a dependency injector but the ease of use of a module system.

React

React has pioneered the virtual DOM and the most fantastic component compose-ability I’ve seen to date. React dominates the UI.

di.js + React

Lets take a look at pairing up di.js & React. While we’re add it lets use the fantastic react-router library.

bootstrap.js

The first thing we need is a bootstrap file, think main() function in Java or C. This just gets the app up and running.

var di = require("di");
var Router = require("./Router");
var React = require("react");

// Make the injector
var injector = new di.Injector([]);

// Grab the Router
var router = injector.get(Router);

// Get it up and running and render it into the DOM
router.run((Handler) => {
  React.render(<Handler />, document.body);
});

Router.js

Now we create the Router and inject the corresponding Routes. Notice we annotate the Router to inform di.js what to inject, the cool thing is though, we could just inject our own Routes manually at test time. Rad, eh?

var ReactRouter = require("react-router");
var di = require("di");
var Routes = require("./routes");

var Router = function (Routes) {
  return ReactRouter.create({
    routes: Routes,
  });
};

// Inject the Routes
di.annotate(Router, new di.Inject(Routes));

module.exports = Router;

Routes.js

Same story here, just define the Routes and inject AppHandler to deal with the base route.

var { Route } = require("react-router");
var AppHandler = require("./AppHandler");
var di = require("di");
var React = require("react");

var Routes = function (AppHandler) {
  return <Route handler={AppHandler} />;
};

di.annotate(Routes, new di.Inject(AppHandler));

module.exports = Routes;

AppHandler.js

Notice, we can inject child components to use in our component.

var React = require("react");
var ChildComponent = require("./ChildComponent");
var di = require("di");

var AppHandler = function (ChildComponent) {
  return React.createClass({
    render() {
      return (
        <div>
          <h1>Hello world!</h1>
          <ChildComponent />
        </div>
      );
    },
  });
};

di.annotate(AppHandler, new di.Inject(ChildComponent));

module.exports = AppHandler;

ChildComponent.js

var React = require("react");
var di = require("di");
var AppActions = require("./AppActions");

var ChildComponent = function (AppActions) {
  return React.createClass({
    handleClick() {
      AppActions.alertInExcitement();
    },
    render() {
      return (
        <div onClick={this.handleClick}>I am a child component. Click me!</div>
      );
    },
  });
};
di.annotate(ChildComponent, new di.Inject(AppActions));
module.exports = ChildComponent;

AppActions.js

One of my favorite pieces here, AppActions has no dependencies so we can just construct an ES6 class.

class AppActions {
  alertInExcitement() {
    alert("I am so excited!");
  }
}

module.exports = AppActions;

Take a look at AppActions, could a file be easier to test? It is literally just a class.(Aside from calling alert, gross.) Notice that even the code that does have dependencies can still be constructed by hand, neat right?

Thoughts

I am going to travel this path a little further and perhaps write an update, di.js could probably use some utility functions for non AtScript code, annotations in long form is a little tedious but other than that I am very satisfied and excited about this technique.

Credits

  1. A conversation a few years ago with Igor Minar, Angular’s tech lead, that convinced me that “service locator” mocking was a bad idea; and that a dependency injector is in fact a very useful tool.
  2. Ryan Florence for create-container and making react-router injector friendly