React Higher Order Components in 3 Minutes

Nov 22, 2018
React, Javascript
~3min
Share on Twitter

Grasp this useful pattern to stop repeating logic in React Components! 😎

What? permalink

A pattern for when we find ourselves repeating logic across components. They are not part of the React API.

What? permalink

They are functions that take components and return new components!

When? permalink

When you’re repeating patterns/logic across components.

Examples;

  • Hooking into/subscribing to a data source
  • Adding interactivity to UI (also achieved with wrapper/render props)
  • Sorting/filtering input data

A Silly Example permalink

We have a Mouse component.


            javascript
            
          const Mouse = () => (
  <span className="mouse" role="img">🐭</span>
)

Let’s make it Draggable using GreenSock’s Draggable module.


            javascript
            
          class Mouse extends Component {
  componentDidMount = () => new Draggable(this.ELEMENT)
  render = () => (
    <span className="mouse" role="img" ref={e => (this.ELEMENT = e)}>🐭</span>
  )
}

We add Cat 😺


            javascript
            
          const Cat = () => (
  <span className="cat" role="img">😺</span>
)

That needs to be Draggable too ✋ Opportunity for a Higher Order Component(HOC).


            javascript
            
          const withDrag = Wrapped => {
  class WithDrag extends Component {
    componentDidMount = () => new Draggable(this.ELEMENT)
    render = () => {
      return (
        <span className="draggable__wrapper" ref={e => (this.ELEMENT = e)}>
          <Wrapped {...this.props} />
        </span>
      )
    }
  }
  WithDrag.displayName = `WithDrag(${Wrapped.displayName || Wrapped.name})`
  return WithDrag
}

Our HOC takes a Component and returns a new Component passing props.

Many HOCs inject new props alongside those passed through. That is often an indicator of whether a HOC is suitable. If we aren’t injecting props, we could likely use a wrapper component or render props.

For our HOC, we could achieve the same result with render props. You could argue that a HOC is not suitable. But this is a “silly” example to get you familiar with looking at HOCs. It’s a little more fun than looking at hooking into a data source too! 😉

Let’s apply this HOC to Cat and Mouse 👍


            javascript
            
          const Mouse = () => (
  <span className="mouse" role="img">🐭</span>
)
const Cat = () => (
  <span className="cat" role="img">😸</span>
)
const DraggableMouse = withDrag(Mouse)
const DraggableCat = withDrag(Cat)

class App extends Component {
  render = () => (
    <Fragment>
      <DraggableCat />
      <DraggableMouse />
    </Fragment>
  )
}

Let’s hook into the onDrag callback and inject the x and y position as props.


            javascript
            
          const withDrag = Wrapped => {
  class WithDrag extends Component {
    componentDidMount = () => new Draggable(this.ELEMENT, { onDrag: this.onDrag })
    onDrag = e => {
      const { x, y } = e.target.getBoundingClientRect()
      this.setState({ x: Math.floor(x), y: Math.floor(y)})
    }
    render = () => {
      return (
        <span className="draggable__wrapper" ref={e => (this.ELEMENT = e)}>
          <Wrapped {...this.props} x={this.state.x} y={this.state.y} />
        </span>
      )
    }
  }
  WithDrag.displayName = `WithDrag(${Wrapped.displayName || Wrapped.name})`
  return WithDrag
}
const Mouse = ({ x, y }) => (
  <span className="mouse" role="img">
    🐭
    {x !== undefined && y !== undefined && (
      <span className="mouse__position">{`(${x}, ${y})`}</span>
    )}
  </span>
)

Now the mouse shows its XY position to the user 🤓

Mouse being dragged around and showing its position

We can also pass props to the HOC. And then filter these out before passing through. For example, passing an onDrag callback.


            javascript
            
          const withDrag = Wrapped => {
  class WithDrag extends Component {
    componentDidMount = () => new Draggable(this.ELEMENT, { onDrag: this.props.onDrag })
    render = () => {
      const { onDrag, ...passed } = this.props
      return (
        <span className="draggable__wrapper" ref={e => (this.ELEMENT = e)}>
          <Wrapped {...passed} />
        </span>
      )
    }
  }
  WithDrag.displayName = `WithDrag(${Wrapped.displayName || Wrapped.name})`
  return WithDrag
}

class App extends Component {
  render = () => (
    <Fragment>
      <DraggableMouse onDrag={e => console.info(e.target.getBoundingClientRect())}>
    </Fragment>
  )
}

By using a HOC, our components remain simple and the logic remains in one place. Our components only care about what is being passed to them. We can reuse them elsewhere without them being draggable. This makes things easier to maintain.

DO’s 👍 permalink

  • Use when there’s an opportunity to package up a repeating pattern
  • Make debugging easier by updating the displayName of the returned Component
  • Pass through props unrelated to the HOC

DON’Ts 👎 permalink

  • Overuse. Another pattern may be more appropriate.
  • Mutate the original Component
  • Use inside the render method

NOTES ⚠️ permalink

  • refs aren’t passed through
  • static methods must be copied over
  • Most HOCs can be written with render props and vice versa!

That’s it! permalink

This is a super small intro to Higher Order Components.