Hi, I'm Merrick and I love code so much I married it. Can provide proof upon request.
Nov 15th 2014
4 min read

React Dependency Injection

Update June 5, 2016

React has removed automatically exporting factory functions from createClass. This means, components can no longer be called as functions that accept their properties. In order to get the same effect, you must call React.createFactory before calling your component as a function.

This:

var MyComponent = React.createClass({
  render() {
    return <h1>Hello {this.props.name}</h1>;
  }
});

MyComponent({
  name: 'Merrick'
});

Becomes this:

const MyComponent = React.createFactory(class extends React.Component {
  render() {
    return <h1>Hello {this.props.name}</h1>;
  }
});

MyComponent({
  name: 'Merrick'
});

To be clear, the recommended use in your application's code is to leverage JSX. In order to follow along with this article, without the overhead of JSX, components should be wrapped in createFactory. (Which JSX effectively does for you by compiling to React.createElement). See this deprecation notice for more information.

React Dependency Injection

I love React.js. I find it to be a powerful tool for creating UI and revel in its immediate mode rendering model. Unforunately however, the dominant approach for writing React applications is use singletons at the module level. Here is an example of what I mean:

var count = 0;

module.exports = {
  increment() {
    return count++;
  },

  getCount() {
    return count;
  }
};

The trouble with this approach is that it is difficult to test. You typically need to add some sort of reset functionality to your module, like this:

var count = 0;

module.exports = {
  increment() {
    return count++;
  },

  getCount() {
    return count;
  },

  reset() {
    count = 0;
  }
};

That way in your unit tests you can reset the corresponding state to its original place. This can get very complex and tedious depending on how much or how complex the state in your module is. Because of this people tend to just throw away the entire module and re-evaluate it each time. That way, each test gets the benefit of fresh state, and you don't have to write reset methods. This is the way Facebook's Jest works and my own library, Squire.js. This is problematic for a few reasons.

  1. It's slower, you are reevaluating the module several times.
  2. The module still has the same state for an entire test file, multiple it() blocks would still need to reset state or require() the module in each it() block. Slow and difficult to test.
  3. require is a service locator, not a dependency injector. Using it for both conflates it's use violating the single responsibility principle.
  4. In tests uses of instanceof can break because you could be getting a different instance for each require().
  5. Code is now encouraged to be written in the form of singletons which is problematic for it's own reasons.

A Different Approach

I wanted to offer a different approach that seems to solve the requirements I have which are:

  1. No singletons, re-evaluation or reset methods required for tests.
  2. Code must remain easy to understand.

React Properties

The method for passing anything to a React Element is to use properties, properties are passed in as the first argument of a React Element. For example:

var MyComponent = React.createClass({
  render() {
    return <h1>Hello {this.props.name}</h1>;
  }
});

MyComponent({
  name: 'Merrick'
});

This would render the following HTML:

<h1>Hello Merrick</h1>

Properties are effectively the technique one can use to pass things to the component which are not child elements. The neat thing is you can even set default properties. Check this out:

var MyComponent = React.createClass({
  getDefaultProps() {
    return {
      name: 'Scuba Steve'
    };
  },
  render() {
    return <h1>Hello {this.props.name}</h1>;
  }
});

MyComponent();

This would render what you would expect:

<h1>Hello Scuba Steve</h1>

Have you made the connection yet? We can use React Properties to inject dependencies to our components. Check this out:

var MyComponentViewModel = require('./MyComponentViewModel');
var HTTP = require('http');

var MyComponent = React.createClass({
  getDefaultProps() {
    return {
      model: new MyComponentViewModel(new HTTP())
    };
  },

  getInitialState() {
    return this.props.model.getState()
  },

  render() {
    return <h1>Hello {this.state.name}</h1>;
  }
});

MyComponent();

Now, the code is just as tractable as it is using the singleton approach, you can see right where the dependencies exist on the file system, but here is where it gets cool... We can pass in a different view model under test, like this:

var MyComponentViewModel = require('./MyComponentViewModel');
var mockHTTP = {
  get: function() {
    // Would probably return a promise.
    return {
      name: 'Async Name'
    };
  }
}

MyComponent({
  model: new MyComponentViewModel(mockHTTP)
});

With this approach we get the following benefits:

  1. We can specify default implementations of dependencies.
  2. We can inject different dependencies if we would like (for example under test).
  3. The code is just as tractable as it is using modules as singletons.
  4. We are coding to an interface not an implementation.
  5. No more re-evaluation of code for tests or global state.

Bonus

A neat side-effect of this approach is that we can use propTypes to validate our dependencies honor a specific interface. This encourages us to code to an interface, not and implementation.

var MyComponent = React.createClass({
  propTypes: {
    model: React.PropTypes.shape({
      getState: React.PropTypes.func
    })
  },

  getDefaultProps() {
    return {
      model: new MyComponentViewModel(new HTTP())
    };
  },

  getInitialState() {
    return this.props.model.getState()
  },

  render() {
    return <h1>Hello {this.state.name}</h1>;
  }
});

This will validate that our model has a getState method! How cool is that!? Now we are really coding to an interface and we get that validated by React's type system.

Real Talk

This is a new idea and I'm not positive how great it is. I would love to hear critisism and feedback in all its forms, preferablly twitter or email.