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