Higher-order components in React

Published by at 16th February 2019 7:00 pm

In the last few weeks I've been working on a big rebuild of the homepage of the legacy application I maintain. As I've been slowly transitioning it to use React on the front end, I used that, and it's by far the largest React project I've worked on to date. This has pushed me to use some more advanced React techniques I hadn't touched on before. I've also had to create some different components that have common functionality.

React used to use mixins to share common functionality, but the consensus is now that mixins are considered harmful, so they have been removed. Instead, developers are encouraged to create higher-order components to contain the shared functionality.

A higher-order component is a function that accepts a React component as an argument, and then returns another component that wraps the provided one. The shared functionality is defined inside the wrapping component, and so any state or methods defined in the wrapping component can then be passed as props into the wrapped one, as in this simple example:

1import React, { Component } from 'react';
2
3export default function hocExample(WrappedComponent) {
4 class hocExample extends Component {
5 constructor(props) {
6 this.state = {
7 foo: false
8 };
9 this.doStuff = this.doStuff.bind(this);
10 }
11 doStuff() {
12 this.setState({
13 foo: true
14 });
15 }
16 render() {
17 return (
18 <WrappedComponent foo={this.state.foo} doStuff={this.doStuff} />
19 );
20 }
21 }
22 return hocExample;
23}

If you've been working with React for a while, even if you haven't written a higher-order component, you've probably used one. For instance, withRouter() from react-router is a good example of a higher-order component that forms part of an existing library.

A real-world example

A very common use case I've come across is handling a click outside of a component. For instance, if you have a sidebar or popup component, it's common to want to close it when the user clicks outside the component. As such, it's worth taking the time to refactor it to make it reusable.

In principle you can achieve this on any component as follows:

  • The component should accept two props - an active prop that denotes whether the component is active or not, and an onClickOutside() prop method that is called on a click outside
  • On mount, an event listener should be added to the document to listen for mousedown events, and it should be removed on unmount
  • When the event listener is fired, it should use a ref on the component to determine if the ref contains the event target. If so, and the status is active, the onClickOutside() method should be called

Moving this to a higher order component makes a couple of issues slightly more complex, but not very. We can't easily get a ref of the wrapped component, so I had to resort to using ReactDOM.findDOMNode() instead, which is potentially a bit dodgy as they're talking about deprecating that.

1import React, { Component } from 'react';
2import { findDOMNode } from 'react-dom';
3
4export default function clicksOutside(WrappedComponent) {
5 class clicksOutside extends Component {
6 constructor(props) {
7 super(props);
8 this.setWrapperRef = this.setWrapperRef.bind(this);
9 this.handleClickOutside = this.handleClickOutside.bind(this);
10 }
11 componentDidMount() {
12 document.addEventListener('mousedown', this.handleClickOutside);
13 }
14 componentWillUnmount() {
15 document.removeEventListener('mousedown', this.handleClickOutside);
16 }
17 setWrapperRef(node) {
18 this.wrapperRef = node;
19 }
20 handleClickOutside(event) {
21 const {target} = event;
22 if (this.wrapperRef && target instanceof Node) {
23 const ref = findDOMNode(this.wrapperRef);
24 if (ref && !ref.contains(target) && this.props.active === true) {
25 this.props.onClickOutside();
26 }
27 }
28 }
29 render() {
30 return (
31 <WrappedComponent {...this.props} ref={this.setWrapperRef} />
32 );
33 }
34 };
35 return clicksOutside;
36}

Now we can use this as follows:

1import React, { Component } from 'react';
2import ReactDOM from 'react-dom';
3import Sidebar from './src/Components/Sidebar';
4import clicksOutside from './src/Components/clicksOutside';
5
6const SidebarComponent = clicksOutside(Sidebar);
7
8function handleClickOutside() {
9 alert('You have clicked outside');
10}
11
12ReactDOM.render(
13 <SidebarComponent
14 links={links}
15 active={true}
16 onClickOutside={handleClickOutside}
17 />,
18 document.getElementById('root')
19);

Higher order components sound a lot harder than they actually are. In reality, they're actually quite simple to implement, but I'm not sure the documentation is necessarily the best example to use since it's a bit on the complex side.