Thursday, 18 May, 2017 UTC


Summary

Recently I've started using ReactJS for personal projects and workshops, I prefer it to Vue for multiple reasons but at the risk of starting a holy war I won't go into that here, maybe that's a future post.
One thing that was bugging me though was the lack of any kind of dependency injection, I like dependency injection. I like Angular for embracing dependency injection. So in this post I want to share a method of a kind-of sort-of DI method I've been using in React.
Why?
Because Inversion of Control, because it makes testing easier, because why not?
Seriously, it helps with unit testing. Please don't tell me you still aren't writing unit tests. Yes, I'm looking at you, Anthony.
The idea of Dependency Injection (hereby referred to as DI because I'm lazy efficient) is that, in a nutshell, you depend on abstractions rather than concrete implementations. In a grown up language like C# [1] this is where interfaces come in really handy, but that doesn't mean we can't follow the concept in React.
[1] That was a joke, JavaScript is for grown ups too.
A Heroes Component
Say, for example, we have the following component:
import React, { Component } from 'react'
import HeroesService from './HeroesService'

class Heroes extends Component {
    constructor(props) {
        super(props)

        let heroesService = new HeroesService();
        this.heroes = heroesService.getAll()
    }

  render() {
    let listOfHeroes = this.heroes.map((hero) => {
      return <tr>
        <td>{hero.name}</td>
        <td>{hero.alias}</td>
      </tr>
    })

    return <table>
              <thead>
                <tr>
                   <th>Hero Name</th>
                  <th>Hero Alias</th>
                </tr>
              </thead>
              <tbody>
                {listOfHeroes}
              </tbody>
          </table>
  }
}

export default Heroes
Simple enough, we're just outputting a list of heroes' names and aliases that we're getting from a heroesService that is instantiated in the constructor.
Here's the HeroesService class for completeness:
export default class {
    getAll() {
        return [
            { name: 'Batman', alias: 'Bruce Wayne' },
            { name: 'Spiderman', alias: 'Peter Parker' },
            { name: 'Wonder Woman', alias: 'Diana Prince' },
            { name: 'Iron Man', alias: 'Tony Stark' }
        ]
    }
}
The problem we now have, is that we have coupled our HeroesComponent to a concrete implementation of HeroesService. This is an issue when it comes to testing this component: we can no longer test this component in isolation, it must use the concrete HeroesService implementation.
This is an even bigger problem if the HeroesService were to be getting the data from a service via HTTP as our tests would also need to go out and grab the data, making them slower and ultimately a lot more flakey.
Instead it'd be great if we could inject our own implementation of the HeroesService into the HeroesComponent.
Kind-of sort-of DI
Well, turns out we can just something sort of like DI using Component.defaultProps, where we can set up the service we need as a property of the component, and give it a default if it is not specified when adding the component to the page. This looks like the following:
Heroes.defaultProps = {
  heroesService: new HeroesService()
}
We simply add this code after the implementation of our HeroesComponent class, just before the line where we export it. Here we are saying that the default value for the heroesService property of this component is a new instance of HeroesService. We can override this whenever we need to with something else, like for instance a fake version of it in our tests.
That means instead of creating an instance of it in componentDidMount we can instead do the following:
//... Rest of the component code

componentDidMount() {
  this.heroes = this.props.heroesService.getAll()
}

//... Rest of the component code
Here we are just grabbing the heroesService property and calling getAll() on it, then setting the result to the heroes property of the class.
If you run the app at this point, it will remain the same. This is because we haven't specified an implementation of HeroesService to use, so it will just use the default instead.
Using a Fake Heroes Service
Lets wrap all of this up by writing a unit test for our HeroesComponent while injecting in a fake service. So we know exactly what data will be added to the component.
import React from 'react'
import {shallow} from 'enzyme'
import Heroes from './App'

let fakeHeroes = [
    { name: 'Fakey McFakeFake', alias: 'Fake Fakerton' },
    { name: 'The Human Fake', alias: 'Faik Fakerman' }
]

class FakeHeroesService {
    getAll() {
        return fakeHeroes
    }
}

test('The heroes component shows a table containing all of the heroes', () => {
    let component = shallow(<Heroes heroesService={new FakeHeroesService()} />)
    expect(component.find('tbody > tr').length).toBe(fakeHeroes.length)
})
Easy! All we need to do is create a FakeHeroesService class that mimics the HeroesService class in its implementation, and then pass an instance of that to the HeroesComponent in the test. The beauty of this being that you have total control over the data that is returned from the fake service. To reiterate, this is incredibly useful if you have a service that is getting data from an API via HTTP requests and the like, or just to help you isolate your tests into only running the code you want to test against.
This helps keep your tests lean, quick, and reliable.