React Design Pattern - Higher-Order Component

✅ Common Pattern of the Recurring Problem

Sometimes, you want to enhance components with additional logic while minimizing the amount of copy-and-pasting. Additionally, it's essential to ensure that the logic is detached from the presentation layer and that the original codebases remain uncontaminated by cross-cutting concerns such as logging and themes.

✅ Solution

Higher-Order Component

Higher-Order Functions are functions that take another function as an argument and return another function. Inspired by Higher-Order Functions in JavaScript, React's Higher-Order Component wraps the original component and returns a new component.

export const withClickLogger = (Component) => {
  // return a new Component that renders the <Component />
  return (props) => {
    const onClick = () => {
      props.onClick();
      console.log("Logging click event...");
    };
    return <Component {...props} onClick={onClick} />;
  };
};

Higher-Order Component (a.k.a HOC)

  • receives another component as an argument,
  • applies additional logic to the passed component,
  • and returns a new component with additional logic.

The Higher-Order Component pattern has certain tradeoffs.

🟢 Above all, it helps to separate concerns. For example, you can hide state-management logic from presentational components. Consider the following withLoader implemented by Lydia.

import React from 'react';
import { LoadingSpinner } from '../components/LoadingSpinner';

export default function withLoader(Element, url) {
  return (props) => {
    /* Add logic to:
    1. Fetch data from the url that was passed as an argument.
    2. Show the <LoadingSpinner /> while the  data is being fetched.
    3. Pass the fetched data to the wrapped component.
    */

    const [data, setData] = React.useState([]);
    const [loading, setLoading] = React.useState(true);

    React.useEffect(() => {
      fetch(url)
        .then((res) => res.json())
        .then((res) => {
          setData(res.listings);
          setLoading(false);
        })
        .catch((err) => setLoading(false));
    }, []);

    if (loading) return <LoadingSpinner />;
    if (!data.length) return null;

    return <Element {...props} data={data} />;
  };
}
import React from 'react';
import { Listing } from './Listing';
import { ListingsGrid } from './ListingsGrid';
import withLoader from '../../hoc/withLoader';

// export function Listings({ listings }) {
export function Listings(props) {
  // remove state-management logic from Listings Component
  // const [listings, setListings] = React.useState([]);

  // React.useEffect(() => {
  //   fetch('https://house-lydiahallie.vercel.app/api/listings')
  //     .then((res) => res.json())
  //     .then((res) => setListings(res.listings));
  // }, []);

  if (!props.data.length) return null;

  // Listings only cares about presentation
  return (
    <ListingsGrid>
      {props.data.map((listing, idx) => (
        <Listing key={idx} listing={listing} />
      ))}
    </ListingsGrid>
  );
}

// export default Listings;
// Loader functionality is injected via HOC
export default withLoader(
  Listings,
  'https://house-lydiahallie.vercel.app/api/listings'
);

🟢 HOCs are reusable,

🟢 which also makes it easier to test them. When you write a unit test that tests complicated logic, you can simply test the container component with state-management logic, which is HOC, instead of testing the entire component including UI.

❌ However, Higher-Order Component is hard to write and understand. This is one of the reasons many libraries are switching to hooks. For example, Material-UI's Less-based legacy styling solution offers APIs for the same functionality in 3 different styles: HOC, hooks, and Styled-Components. Let's compare the HOC and the hooks API:

import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

const styles = {
  root: {
    background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
    border: 0,
    borderRadius: 3,
    boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)',
    color: 'white',
    height: 48,
    padding: '0 30px',
  },
};

function HigherOrderComponent(props) {
  const { classes } = props;
  return <Button className={classes.root}>Higher-order component</Button>;
}

HigherOrderComponent.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default withStyles(styles)(HigherOrderComponent);
import * as React from 'react';
import { makeStyles } from '@mui/styles';
import Button from '@mui/material/Button';

const useStyles = makeStyles({
  root: {
    background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
    border: 0,
    borderRadius: 3,
    boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)',
    color: 'white',
    height: 48,
    padding: '0 30px',
  },
});

export default function Hook() {
  const classes = useStyles();
  return <Button className={classes.root}>Hook</Button>;
}

In the HOC API example, it is not easy to understand at first glance what withStyles(styles) does. In contrast, in the hooks example, it's easier to grasp that useStyles() returns a class name that references style rules defined earlier. Such simplicity significantly facilitates development and debugging.

Higher-Order Component patterns were predominantly used in conjunction with class-style components in the past. Just like Material-UI, many libraries have incorporated Hooks APIs into their HOC equivalents, and in some cases, have even replaced them entirely (e.g. react-router withRouter() vs. useRouter(), react-redux connect(mapStateToProps, mapDispatchToProps)([COMPONENT]) vs. useSelector(), useDispatch(), etc.). Does it mean that Higher-Order Components are solely intended for backward compatibility?

// 여기서부터

🟢 One clear advantage of HOCs over hooks is their ability to encapsulate logic. By using an HOC, you can inject new functionality into an existing component without copy-and-pasting code or modifying the original codebase. This approach makes it easier to adhere to the Open-Closed Principle, as you can extend the functionality of a component without modifying its source code.

Following Nadia's lead in her blog post, imagine you want to add a log function to different types of UI components. The first solution that comes to mind is to use hooks.

const Button = ({ onClick, loggingData }) => {
  const log = useLogger()

  const onButtonClick = () => {
    log('Button was clicked', loggingData)
    onClick()
  }

  return <button onClick={onButtonClick}>{children}</button>
}
const ListItem = ({ onClick, loggingData }: ListItemProps) => {
  const log = useLogger()
  
  const onListItemClick = () => {
    log('List item was clicked', loggingData)
    onClick()
  }
  
  return <Item onClick={onListItemClick}>{children}</Item>
}

As the list of components that need logging increases, you end up duplicating the same code just as much. When there's a change, you again have to visit every place where useLogger() is called, which makes the application highly vulnerable to errors.

Now let's look at the HOC solution:

export const withLoggingOnClick = (Component, params) => {
  return (props) => {
    const onClick = () => {
	  console.log('Log on click: ', params.text);
      props.onClick()
    }

    return <Component {...props} onClick={onClick} />;
  }
}

Now, whenever there's a UI component that needs logging, you don't have to care about its internal logic. Simply wrap it with withLoggingOnClick().

const Button = () => {
   /* ... */
}

export default withLoggingOnClick(Button, { text: 'button component' })
const ListItem = () => {
  /* ... */
}

export default withLoggingOnClick(ListItem, { text: 'list item component' })
import Button from './components/Button'
import ListItem from './components/ListItem'

const App = () => {
  const onButtonClick = () => {
  	console.log('App clicked Button')
  }
  
  const onListItemClick = () => {
  	console.log('App clicked ListItem')
  }

  return (
    <div>
      <Button onClick={onButtonClick}>
      	Click me
      </Button>
      <ListItem onClick={onListItemClick}>Item # 1</ListItem>
      <ListItem onClick={onListItemClick}>Item # 2</ListItem>
      <ListItem onClick={onListItemClick}>Item # 3</ListItem>
    </div>
  )
}

✅ Exercises

  • ex 1) withMountLogger()
    You can also hook additional logic to a component's lifecycle method

References

Hallie, Lydia. (2022, August 18). A Tour of JavaScript & React Patterns [video file]. Retrieved from https://frontendmasters.com/courses/tour-js-patterns/
"Higher-Order Components in React Hooks Era." Makarevich, Nadia. 2022
"Higher-Order Components in React." Akintayo, Shedrack. 2020
@mui/styles

Subscribe to go-kahlo-lee

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe