Higher-Order Components
Pass reusable logic down as props to components throughout your application
Overview
Higher-Order Components (HOC) make it easy to pass logic to components by wrapping them.
For example, if we wanted to easily change the styles of a text by making the font larger and the font weight bolder, we could create two Higher-Order Components:
withLargeFontSize
, which appends thefont-size: "90px"
field to thestyle
attribute.withBoldFontWeight
, which appends thefont-weight: "bold"
field to thestyle
attribute.
Any component that's wrapped with either of these higher-order components will get a larger font size, a bolder font weight, or both!
Implementation
We can apply logic to another component, by:
- Receiving another component as its
props
- Applying additional logic to the passed component
- Returning the same or a new component with additional logic
To implement the above example, we can create a withStyles
HOC that adds a color
and font-size
prop to the component's style.
export function withStyles(Component) {
return (props) => {
const style = {
color: "red",
fontSize: "1em",
// Merge props
...props.style,
};
return <Component {...props} style={style} />;
};
}
We can import the withStyles
HOC, and wrap any component that needs styling.
import { withStyles } from "./hoc/withStyles";
const Text = () => <p style={{ fontFamily: "Inter" }}>Hello world!</p>;
const StyledText = withStyles(Text);
If you have a component that always needs to be wrapped within a HOC, you can also directly pass it instead of creating two separate components like we did in the example above.
const Text = withStyles(() => (
<p style={{ fontFamily: "Inter" }}>Hello world!</p>
));
Tradeoffs
Separation of concerns: Using the Higher-Order Component pattern allows us to keep logic that we want to re-use all in one place. This reduces the risk of accidentally spreading bugs throughout the application by duplicating code over and over, potentially introducing new bugs each time
Naming collisions: It can easily happen that the HOC overrides a prop of a component. Make sure that the HOC can handle accidental name collision, by either renaming the prop or merging the props.
function withStyles(Component) {
return props => {
const style = {
padding: '0.2rem',
margin: '1rem',
// Merge props
...props.style
}
return <Component {...props} style={style} />
}
}
// The `Button` component has a `style` prop, that shouldn't get overwritten in the HOC.
const Button = () = <button style={{ color: 'red' }}>Click me!</button>
const StyledButton = withStyles(Button)
Readability: When using multiple composed HOCs that all pass props to the element that's wrapped within them, it can be difficult to figure out which HOC is responsible for which prop. This can hinder debugging and scaling an application easily.
Exercise
If we have a lot of components that fetch data, we might want to show a certain loader while they're still loading data.
In that case, we might want to create a withLoader
HOC, which returns a component that fetches data, and returns a LoadingSpinner
component while it's fetching data.
Challenge
Complete the withLoader
HOC, that shows a spinner when a component is still loading data.