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.
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.
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; |
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; |
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(); | |
}); |
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.
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.
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; |
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; |
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; |
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(); | |
}); |
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!
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.
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(); | |
}); | |
}); | |
}); | |
}); |
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.
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'); | |
}); | |
}); | |
}); |
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.
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!