Why doesn’t React come with dependency injection? Short answer: Because it doesn’t need it and also because it is not its responsibility.

But how do you isolate your components during testing then? How do you achieve inversion of control for dependencies when you do not have a dependency injection container?

Let’s have a look…

Intro (TL;DR)

You are watching the third video of a series about doing TDD when developing a React webapp. I made this series and the underlying app because I teach React trainings and also TDD trainings, and I needed some new examples for that.

The videos themselves will not give you everything you need to implement your own webapp from scratch, but only show you some interesting aspects of doing TDD. If you want to know more, look at the code and commit history of the webapp on GitHub, which I’ll link in the description.

Today I want to talk about managing the dependencies of your components by using “inversion of control” and also about isolating your components for testing.

Question / Problem

The webapp I am talking about is a simple “kitchen timer”. It only contains a few components, but I still want to isolate them from their business logic in my tests.

The “App” component renders a “Timer”, which is shown as “Anonymous” here in the DevTools. We’ll see in a minute, why. The Timer renders a configuration panel and the graphics itself, which is an SVG containing a “Background”, the red “Slice” and a “Foreground”.

The timer and its react components

The “Config” and the “Slice” call some business logic, and I want to isolate them from it. I want to inject the business logic into the components. In this case, I do not want to inject other components, because enzyme already gives me some isolation from other components when testing a component.

Solution

I tried two variants for injecting the dependencies:

  • Injecting them directly into the props of the component
  • Using a function to generate the component

In the first variant, I pass functions to the component via it’s props - functions that the component will call later. I could also pass objects, but I do not need that right now.

I do not want to pass in the functions every time in my production code, so I assign default values to some of the props - default values for the dependencies I want to inject.

export function Foreground({circleSegment = segmentTo}) {
	//...
	return (
		<g>
			{minutesLines}
			<circle className="foreground" cx="0" cy="0" r="1" />
		</g>
	);
}

In the case of the Foreground component, the dependency is the function circleSegment and the default implementation is segmentTo - a function that contains the production code. Foreground needs that to calculate where to draw the small lines, given a percentage point on the circle.

In the production code, I can use the component directly, only passing in the “real” props - i.e. the props that are not injected dependencies. Those will come from the default values. In this case, the foreground component only has dependencies, no other props.

<svg key='pie' className="timer" viewBox="-1.1 -1.1 2.2 2.2">
	<Background />
	<Slice percentLeft={timeToPercentage(mins, secs)}/>
	<Foreground />
</svg>

In my tests, I can pass in stubbed functions using the extra props, end then test whether those functions were called.

it('draws a line at '+mins+' minutes', () => {
	const circleSegment = (_, radius=1) => {
		if(radius === 1) {
			return [12.5, 18.3];
		}
		return [22.5, 28.3];
	};
	const slice = shallow(<Foreground circleSegment={circleSegment} />);

	expect(slice.find(`.minutes-line-${mins}`).at(0).prop('d')).to.equal('M 12.5 18.3 L 22.5 28.3');
});

The code that tests the foreground component

In the test code, I stub circleSegment to only return two canned values - one when radius is “1” and a different value otherwise: I want to test the behavior of the caller at those two points. The test itself is a parameterized test that is run with different values of the parameter mins.

The second variant I tried was to create the component using a function, and that function gets the dependencies - again with default values.

I create the “production” Slice with the function createSlice, which again gets a dependency called circleSegment. The default implementation is again segmentTo. The Slice also draws something at different percentage points of the circle, in this case, the red pie slice.

export const Slice = createSlice();

export function createSlice(circleSegment = segmentTo) {
	return ({percentLeft}) => {
		//...
		return <path className="slice" d={pathData} />
	};
}

The code that creates the slice component

This function creates a react component - in this case, only a render function - that has a single “prop”. So, both the production code and the test code use the component with only this parameter:

<svg key='pie' className="timer" viewBox="-1.1 -1.1 2.2 2.2">
	<Background />
	<Slice percentLeft={timeToPercentage(mins, secs)}/>
	<Foreground />
</svg>

Code showing how the slice component is used in the production code and in the test code

Here we also see why the component is shown as “Anonymous” in the React DevTools. It is an anonymous function. You can fix that by giving the returned function a name, but that makes the createSlice function slightly more complicated:

export function createSlice(circleSegment = segmentTo) {
	function Slice({percentLeft}) {
		//...
		return <path className="slice" d={pathData} />
	};
	return Slice;
}

The test code creates the component differently, passing in a stubbed (or in this case, faked) implementation of circleSegment.

it('draws the pie slice with data from circleSegment', () => {
	const circleSegment = sinon.fake.returns([12.5, 18.3]);
	const SliceComp = createSlice(circleSegment);
	const slice = shallow(<SliceComp />);

	expect(slice.find('path')).to.have.length(1);
	expect(slice.find('path').at(0).prop('d')).to.equal('M 12.5 18.3 A 1 1 0 0 1 0 -1 L 0 0');
});

Code for testing the slice component

Conclusion

To recap… Creating the component directly, with some props that are the dependencies, is easier to implement. Especially when you use function components, where the component itself is just a render function. Assign default values to some of the props - the props that are dependencies - and that’s it.

You will not pass those props in your production code, but you can do so during testing. It’s easier to implement, but somehow it seems to be a little bit less “clean” to me.

The other option is to have a function that creates the component. That’s a little bit more code to write, but it separates the responsibilities better: The component is only responsible for rendering and maybe state management, the create function is responsible for the dependencies.

One could even combine the two approaches: Have a create function that gets the dependencies and passes them as “props” to the component, like Redux’ connect does.

CTA

How do you manage the dependencies of your React components? How do you isolate the components for testing? Tell me in the comments of this video! And also, if you have any questions, ask them in the comments and I will try to answer them.

Subscribe to this youtube channel, so you do not miss the next video, where I will show you how I test-drive some of the business code: A well-known formula that I still want to TDD to get all edge cases right. Also follow me on Twitter, I am @dtanzer there. And check out our training offers at devteams.at/services/training where we provide React and TDD trainings.

Read / watch all parts of “React TDD” here: