Sharing and Testing Code in React with Higher Order Components

Ken Shimizu ·

Higher Order Components (HoC) in React can be simple to use and test. If you haven’t read about HoC’s and how and why they’ve replaced mixins, check out this great Medium post by Dan Abramov.

Most of the resources and examples that I found online about higher order components are complex, and don’t include a testing solution. This is a super simple example designed to demonstrate how we can generalize React components using Higher Order Components and unit test them. We will be using ES6 syntax and Enzyme to test.

No HoC’s

Let’s start the example with two Components, a UserList and a PokemonList. In this naive example, the two lists have a different number of columns, and different column headers, but share the same selection behavior.

Components

var UserList = React.createClass({
getInitialState() {
return {
users: [],
selection: new Set(),
};
},
fetchUsers() {
// Hit API, get users, and set to users on state.
// Assume unsharable for… reasons.
},
onSelect(user) {
if(this.state.selection.has(user)) {
this.state.selection.delete(user);
} else {
this.state.selection.add(user);
}
},
renderUsers() {
return users.map(function(user) {
return(
<tr>
<td>
<input type="checkbox" onClick={this.onSelect(user)} checked={this.state.selection.has(user)} />
</td>
<td>user.name</td>
<td>user.id</td>
<td>user.numberOfBadges</td>
</tr>
);
});
},
render() {
return (
<table>
<thead>
<tr>
<th></th>
<th>"Name"</th>
<th>"ID"</th>
<th>"# of Badges"</th>
</tr>
</thead>
<tbody>
this.renderUsers();
</tbody>
</table>
);
},
});
export default UserList;

view raw
user_list_no_hoc.jsx
hosted with ❤ by GitHub

var PokemonList = React.createClass({
getInitialState() {
return {
pokemon: [],
selection: new Set(),
};
},
fetchPokemon() {
// Hit API, get pokemon, and set to pokemon on state.
// Assume unsharable for… reasons.
},
onSelect(pokemon) {
if(this.state.selection.has(pokemon)) {
this.state.selection.delete(pokemon);
} else {
this.state.selection.add(pokemon);
}
},
renderPokemon() {
return pokemon.map(function(pokemon) {
return(
<tr>
<td>
<input type="checkbox" onClick={this.onSelect(pokemon)} checked={this.state.selection.has(pokemon)} />
</td>
<td>pokemon.name</td>
<td>pokemon.id</td>
<td>pokemon.types</td>
<td>pokemon.favoriteAttack</td>
<td>pokemon.isShiny</td>
</tr>
);
});
},
render() {
return (
<table>
<thead>
<tr>
<th></th>
<th>"Name"</th>
<th>"ID"</th>
<th>"Type(s)"</th>
<th>"Favorite Attack"</th>
<th>"Shiny?"</th>
</tr>
</thead>
<tbody>
this.renderPokemon();
</tbody>
</table>
);
},
});
export default PokemonList;

Entry

import React from 'react';
import UserList from 'user_list';
import PokemonList from 'pokemon_list';
$(function() {
function render() {
var userList = $('#user-list'),
pokemonList = $('#pokemon-list');
if (userList.length > 0) {
ReactDOM.render(<UserList />, userList[0]);
}
if (pokemonList.length > 0) {
ReactDOM.render(<PokemonList />, pokemonList[0]);
}
}
render();
});

view raw
entry_no_hoc.jsx
hosted with ❤ by GitHub

The code to fetch the data is unsharable for… reasons. Let’s just pretend, ok? Looking at the rest of the class, however, it’s painfully obvious that we need to extract this shared code.

Higher Order Components

Now, let’s extract the selection code! In the example below, have extracted the common code into a higher order component called ListWrapper. The HoC is created inside a function that accepts another component. Inside the function, the HoC is defined and returned.

Components

function listWrapper(ListComponent) {
const ListWrapper = React.createClass({
getInitialState() {
return {
selection: new Set(),
};
},
handleOnSelect(item) {
if(this.state.selection.has(item)) {
this.state.selection.delete(item);
} else {
this.state.selection.add(item);
}
},
render() {
return (<ListComponent
onSelect={this.handleOnSelect}
{this.props}
{this.state}
/>
);
},
});
return ListWrapper;
}
export default listWrapper;

view raw
list_wrapper.jsx
hosted with ❤ by GitHub

var UserList = React.createClass({
propTypes: {
onSelect: React.PropTypes.func.isRequired,
selection: React.PropTypes.object.isRequired,
},
getInitialState() {
return {
users: [],
};
},
fetchUsers() {
// Hit API, get users, and set to users on state.
// Assume unsharable for… reasons.
},
renderUsers() {
return users.map(function(user) {
return(
<tr>
<td>
<input type="checkbox" onClick={this.props.onSelect(user)} checked={this.props.selection.has(user)} />
</td>
<td>user.name</td>
<td>user.id</td>
<td>user.numberOfBadges</td>
</tr>
);
});
},
render() {
return (
<table>
<thead>
<tr>
<th></th>
<th>"Name"</th>
<th>"ID"</th>
<th>"# of Badges"</th>
</tr>
</thead>
<tbody>
this.renderUsers();
</tbody>
</table>
);
},
});
export default UserList;

view raw
user_list_hoc.jsx
hosted with ❤ by GitHub

var PokemonList = React.createClass({
propTypes: {
onSelect: React.PropTypes.func.isRequired,
selection: React.PropTypes.object.isRequired,
},
getInitialState() {
return {
pokemon: [],
};
},
fetchPokemon() {
// Hit API, get pokemon, and set to pokemon on state.
// Assume unsharable for… reasons.
},
renderPokemon() {
return pokemon.map(function(pokemon) {
return(
<tr>
<td>
<input type="checkbox" onClick={this.props.onSelect(pokemon)} checked={this.props.selection.has(pokemon)} />
</td>
<td>pokemon.name</td>
<td>pokemon.id</td>
<td>pokemon.types</td>
<td>pokemon.favoriteAttack</td>
<td>pokemon.isShiny</td>
</tr>
);
});
},
render() {
return (
<table>
<thead>
<tr>
<th></th>
<th>"Name"</th>
<th>"ID"</th>
<th>"Type(s)"</th>
<th>"Favorite Attack"</th>
<th>"Shiny?"</th>
</tr>
</thead>
<tbody>
this.renderPokemon();
</tbody>
</table>
);
},
});
export default PokemonList;

view raw
pokemon_list_hoc.jsx
hosted with ❤ by GitHub

Entry

import React from 'react';
import UserList from 'user_list';
import PokemonList from 'pokemon_list';
import listWrapper from 'list_wrapper';
$(function() {
function renderList(ListComponent, $domNode) {
var List = listWrapper(ListComponent);
ReactDOM.render(<List />, $domNode[0]);
}
function render() {
var userList = $('#user-list'),
pokemonList = $('#pokemon-list');
if (userList.length > 0) {
renderList(UserList, userList);
}
if (pokemonList.length > 0) {
renderList(PokemonList, pokemonList);
}
}
render();
});

view raw
entry_hoc.jsx
hosted with ❤ by GitHub

Notice that selection and onSelect used to be state values on the original components, but are now props that are passed from the ListWrapper. In the entry file, we are now calling the listWrapper function on our components and rendering that into the DOM rather than invoking render on our component directly. Do not wrap your components before this! It won’t allow you to unit test all parts individually.

That’s it, really. What’s that? You want to unit test your code you say? That’s what I like to hear!

Testing!

To unit test these components, we will use a AirBnB’s shallow rendering library, Enzyme.

Again, I have not wrapped the list components before exporting them above. This allows us to test the HoC and component behavior separately.

Testing the HoC

import React from 'react';
import "babel-polyfill";
import listWrapper from 'list_wrapper';
import { shallow } from 'enzyme';
describe('ListWrapper', function () {
var wrapper, ListComponent, MockListComponent, instance, set;
beforeEach(function () {
MockListComponent = React.createClass({
render: function () {
return (<div>Fake List</div>);
},
});
set = new Set();
WrapperComponent = listWrapper(MockListComponent);
wrapper = shallow(<ListComponent />);
instance = wrapper.instance();
});
it('renders the List Component as the root element', function () {
expect(wrapper.first().is(MockListComponent)).toBeTruthy();
});
describe('handleOnSelect', function () {
describe('when not already selected', function () {
it('adds the key to the selection set', function () {
instance.handleOnSelect('1234');
expect(instance.state.selection.has('1234')).toBeTruthy();
});
});
describe('when already selected', function () {
beforeEach(function () {
instance.setState({selection: new Set(['2314'])});
});
it('removes the selection from the set', function () {
instance.handleOnSelect('2314');
expect(instance.state.selection.has('1234')).toBeFalsy();
});
});
});
});

view raw
list_wrapper_spec.js
hosted with ❤ by GitHub

In order to unit test the ListWrapper, I create a MockListComponent with no behavior in the before block. I wrap the dummy component with using the listWrapper function, and then use Enzyme’s shallow to shallow render it into the DOM.

The rest is just Jasmine.

Testing the Components

import React from 'react';
import UserList from 'user_list';
import { shallow } from 'enzyme';
describe('UserList', function () {
var set, onSelectSpy, wrapper, instance;
beforeEach(function () {
set = new Set();
onSelectSpy = jasmine.createSpy('onSelect');
wrapper = shallow(<UserList set={set}
onSelect={onSelectSpy}/>);
instance = wrapper.instance();
});
it('renders table as the root element', function () {
expect(wrapper.is('table')).toBeTruthy();
});
describe('some method where onSelect called at some point', function () {
it('calls the spy', function () {
instance.someMethod();
expect(onSelectSpy).toHaveBeenCalledWith('foo');
});
});
});

view raw
user_list_spec.js
hosted with ❤ by GitHub

Because we didn’t wrap the list components, we can now unit test them as we normally would. We have to pass in our props before we render the component because we are enforcing propTypes.

I added a sample spec just to demo how we would use the onSelect spy if we needed to.

Easy and Testable

Higher Order Components are fairly easy to use and testable. Just make sure you actually need one before using it. Trying to wedge in a HoC where it’s not needed can be a painful process.

Good luck and happy coding!

What can we help you with?

Tell us a bit about your project, or just shoot us an email.

Interested in a Career at Carbon Five? Check out our job openings.