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:
Link to the discussed project is here.
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 has pioneered the virtual DOM and the most fantastic component compose-ability I’ve seen to date. React dominates the UI.
Lets take a look at pairing up di.js & React. While we’re add it lets use the fantastic react-router library.
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);
});
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;
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;
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;
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;
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?
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.