+2

Higher-Order Components (HOC) in React

1. Introduction

In ReactJS, Higher-Order Component (HOC) is a pattern used with the intent to share common functionality between components while eliminating the posibility of DRY. Despite its name including "component" phrase, a Higher-order Component is no component but a function which takes a component as an argument and returns another component, thus, providing the ability to transform components as well as adding additional data or functionality.

2. Higher-order Functions

According to Wikipedia, in Mathematics and Computer Science, a Higher-order Function is a function that either takes one or more function as its arguments or returns another function as the result of both*.

Working on Front-end side, you must have probably been accustomed with using Higher-Order Function in one form or another (or course, that is they way Javascript works, duh...). Passing anonymous functions or callbacks as arguments or a function returning another function as result, all of these fall under the categories of Higher-Order Function. For example,

creating a new array with the results of calling a provided function (callback) on every element,

const arr = [1, 2, 3];
console.log(arr.map(item => item + 1)); // [2, 3, 4]

assign event listener to DOM element by passing callback function,

document.addEventListener('DOMContentLoaded', () => {
  document
    .getElementById('#login-btn')
    .addEventListener('click', () => console.log('Login btn clicked!'));
});

or the infamous Closure pattern.

const subtractCalculator = () => (a, b) => a - b;

const subtract = subtractCalculator();
console.log(subtract(10, 5)); // 5

Let's dig a little deeper in order to have a better perspective of its mechanism and advantages:

const add = (...params) => params.reduce((previous, element) => previous + element, 0));

const multiply = (...params) => params.reduce((previous, element) => previous * element, 1));

const calculator = inputFunc => (...params) => inputFunc(...params);

We have add and multiply functions perform arithmetic operations as per their respective name, while the calculator function accepts functions as its input and return another function as the output. This is exactly a polymorphism of higher order function. All you have to do is plug in add or multiply and calculator will happlily take it from there:


// input functions is invoked with parameters passed down
console.log(calculator(add(1, 2, 3, 4, 5)); // 15
console.log(calculator(multiply(1, 2, 3, 4, 5)); // 120

So in summary, calculator play the role of a container which extends the functionality of add and multiply, giving us the capability of dealing with problems at a higher or more abstract level, hence the name Higher-order Function. In this situation, the advantages of using Higher-order Function include:

  • Enhancement in code readability as well as reusability.
  • Enhancement in maintainability, as you can now add extra functionalit common to all arithmetic operations at the container level.

Ok, now that you have probably have a better view on this, let move on to the next section to discover what the Higher-order Component in React is capable of.

3. Higher-order Components

So how about a Higher-order Component? Similar to that of a higher order function, a Higher-order Component (HOC) is a function that takes a component as an argument and returns a new component.

Two HOC’s implementations that you may be familiar with in the React ecosystem are connect from Redux and withRouter from React Router. The connect function from Redux is used to give components access to the global state in the Redux store, and it passes these values to the component as props. The withRouter function injects the router information and functionality into the component, enabling the developer access or change the route.

Consider you are writing a user comment feature for your application. So you will write something like this.

/*
* comment-box.js
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

import { postComment } from '../actions/comment';

class CommentBox extends Component {
  static propTypes = {
    postComment: PropTypes.func.isRequired
  }
  
  constructor() {
    super();
    
    this.state = {
      username: '',
      comment: ''
    };

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(e) {
    const { name, value } = e.target;
    this.setState({
      [name]: value,
    });
  }

  handleSubmit(e) {
    e.preventDefault();
    const { username, comment } = this.state;
    
    this.props.postComment({ username, comment });
  }
  
  render() {
    const { username, comment } = this.state;

    return (
      <div className="comment-form">
        <form onSubmit={(e) => this.handleSubmit(e)}>
          <div className="form-group">
            <input type="text" name="username" id="username" value={username} onChange={() => this.handleChange(e)} />
          </div>
          <div className="form-group">
            <input type="text" name="comment" id="comment" value={comment} onChange={() => this.handleChange(e)} />
          </div>
          <div className="form-group">
            <button type="submit">Comment</button>
          </div>
        </form>
      </div>
    )
  }
}

const mapStateToProps = () => ({});

const mapDispatchToProps = dispatch => ({
  postComment: dispatch(postComment(data))
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(CommentBox)

As you can figure out from the render method, the representation of this component is a form with 2 input fields and 1 submit button. What if one day, you have to include the exact same functionality but with different UI in a different part of your application. Changes are you might end up creating stateless components for each of the two forms and then pass the handlers as props to these component. A right approach to go with but actually doesn't look so clean, does it? And not cool, either...

Have a look how we can work out the problem by implementing an HOC.

/*
* comment-hoc.js
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

import { postComment } from '../actions/comment';

const CommentHOC = (PassedComponent) => {
  class CommentBoxContainer extends Component {
    static propTypes = {
      postComment: PropTypes.func.isRequired
    }

    constructor() {
      super();

      this.state = {
        username: '',
        comment: ''
      };
      
      this.handleChange = this.handleChange.bind(this);
      this.handleSubmit = this.handleSubmit.bind(this);
    }

    handleChange(e) {
      const { name, value } = e.target;
      this.setState({
        [name]: value,
      });
    }

    handleSubmit(e) {
      e.preventDefault();
      const { username, comment } = this.state;

      this.props.postComment({ username, comment });
    }

    render() {
      const { username, comment } = this.state;
      return (
        <PassedComponent
          username={username}
          comment={comment}
          handleChange={this.handleChange}
          handleSubmit={this.handleSubmit}
        />
      )
    }
  }
  
  const mapStateToProps = () => ({});
    
  const mapDispatchToProps = dispatch => ({
    postComment: dispatch(postComment(data))
  });
  
  return connect(
    mapStateToProps,
    mapDispatchToProps
  )(CommentBoxParent);
}

export default CommentHOC;
/*
* comment-box.js
*/
import React, { Component } from 'react';

import CommentHOC from './CommentHOC';

const CommentBox = ({ username, comment, handleChange, handleSubmit }) => (
  <div className="comment-form">
    <form onSubmit={(e) => handleSubmit(e)}>
      <div className="form-group">
        <input type="text" name="username" id="username" value={username} onChange={(e) => handleChange(e)} />
      </div>
      <div className="form-group">
        <input type="text" name="comment" id="password" value={comment} onChange={(e) => handleChange(e)} />
      </div>
      <div className="form-group">
        <button type="submit">Comment</button>
      </div>
    </form>
  </div>
);

export default (CommentHOC(CommentBox));

and that’s it. No matter how many comment forms you need in you app, just that new comment form component with this CommentHOC and you will be able to access to all the props that you are passing from HOC.

4. Summary

We have learnt the basic concept of HOC - a popular advanced technique for building reusable components. One thing to keep in mind is that a HOC should be a pure function with no side-effects. In another word, it should not make any modifications and just compose the original component by wrapping it in another component.

References: React HOC


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí