Traditionally, engineers use mixins, decorators, inheritance, and plain code duplication to add common functionality to a handful of components. Mixins and decorators can modify the target object in such a way that you are never really sure what methods are safe to override without unwanted side effects. Inheritance can quickly get out of hand as you choose a base class and later discover that you actually need functionality from several base classes. Code duplication increases your technical debt and creates more work later. Sometimes, these patterns are the smart choice to solve your problem, but often, you can solve them more effectively with higher-order components.
Described by the React team as an alternative to using mixins with React, the higher-order component design pattern can help us author reusable, testable components in our web applications. In the simplest terms, a higher-order component is a function that accepts a component, and returns a new component that wraps the original. While traditional mixins and inheritance modify the target component, higher-order components use composition to add new functionality into existing objects.
Let’s look at a scenario where the use of higher-order components can simplify our codebase. The source code samples here use React and assume you have a basic understanding of React and ES6+ features like arrow functions and classes.
Our project
Let’s say we have a simple React web app in which we have three views: homepage, account, and schedule. The account and schedule views reside behind some kind of user authentication mechanism and are only accessible by logged in users.
A common way to accomplish this sort of conditional rendering is by, well, conditionals.
import React, {Component} from 'react';
import LogIn from '../components/LogIn';
import auth from '../utils/auth';
export default class Account extends Component {
render() {
if (auth.isAuthenticated()) {
return (
<div>
<h1>My Account</h1>
</div>
);
} else {
return <LogIn/>;
}
}
}
The render
method of our authorized components check if a user is authorized, and, if so, renders their content. If the user is not authorized, they present a login form instead. This may approach may work fine for the first component, but let’s take a look at another.
import React, {Component} from 'react';
import LogIn from '../components/LogIn';
import auth from '../utils/auth';
export default class Schedule extends Component {
render() {
if (auth.isAuthenticated()) {
return (
<div>
<h2>View Your Schedule</h2>
</div>
);
} else {
return <LogIn/>;
}
}
}
This approach introduces a few code smells:
- The rendering conditional is identical in each component that needs authorization.
- Adding authorization to another component will further duplicate code, and removing authorization from a component requires you to make changes to the component, where there is always a risk of making a mistake that might introduce new bugs.
- The visual component should not need to know how it is being displayed, just that it is.
Enter higher-order components
The good news is that we can fix all of those problems by using higher-order components. Our goal is to separate our authentication logic (choosing when to display the intended content and when to display the login form) from our component.
To declare our higher-order component, we just need to declare a function that accepts an existing component, and returns a new component that wraps the original component with additional logic.
export default function Authorized(WrappedComponent) {
return class extends Component {
render() {
if (auth.isAuthenticated()) {
return <WrappedComponent {...this.props} />;
} else {
return <LogIn/>;
}
}
};
}
Now we can remove the authorization concern from our components and leave them to their intended purpose.
import React, {Component} from 'react';
export default class Account extends Component {
render() {
return (
<div>
<h1>My Account</h1>
</div>
);
}
}
Let’s look at how this works.
- We call the
Authorized
function and pass in the component we want to be reserved for users only. - The
Authorized
function creates a new component and returns it. - The new component performs the same authorization logic we used to use, but now it is only used in one place. Upon authorization, the original, wrapped component, is rendered (passing through any properties that were provided).
To use this new Authorized
function, we just need to call it on our existing components to create the authorized versions.
const authorizedAccount = Authorized(Account);
const authorizedSchedule = Authorized(Schedule);
We now have two new components, authorizedAccount
and authorizedSchedule
that we can use in place of our old components, Account
and Schedule
.
import React, {Component} from 'react';
import './App.css';
import './pages/Homepage';
import Homepage from "./pages/Homepage";
import {Link, Route} from 'react-router-dom'
import Account from "./pages/Account";
import Schedule from "./pages/Schedule";
import Authorized from "./components/Authorized";
const authorizedAccount = Authorized(Account);
const authorizedSchedule = Authorized(Schedule);
const authorizedAccount = Authorized(Account);
const authorizedSchedule = Authorized(Schedule);
class App extends Component {
render() {
return (
<div className="App">
<header className="header">
<Link className="headerLink" to="/">Example App</Link>
<nav>
<Link className="link" to="/account">My Account</Link>
|
<Link className="link" to="/schedule">My Schedule</Link>
</nav>
</header>
<section className="content">
<Route path="/" exact component={Homepage}/>
<Route path="/schedule" component={authorizedSchedule}/>
<Route path="/account" component={authorizedAccount}/>
</section>
</div>
);
}
}
In our small example here, a higher-order component was able to resolve each architecture and code quality problem we mentioned earlier.
First, removing the authorization logic from every other component removes our duplicate code and puts our logic in one, known, component. This makes things easier to test, and easier to troubleshoot.
Second, separating our authorization code has also made our presentational components more stable. If we need to add authorization to a component in the future, all we need to do is wrap it on our Authorized
component. Need to remove authorization from a component? Just unwrap it. We no longer need to update the components when we need/don’t need components to be behind the authorized barrier. Our tests for authorization logic can now dwell with the authorization component rather than be sprinkled around in unrelated places.
Third, our components are simpler. They are no longer concerned with determining if a user is authenticated. Need some logic that is more complex? That’s easy too. Since it’s just a function call, we can easily add more complex features to our Authorized
component. Imagine you had components that were allowed only for admins, and not regular users? All we need to do is add that logic to Authorized
, and leave our other components alone.
const adminPage = Authorized(SomePage, 'admin');
That’s great.. but my authentication system isn’t like this
Higher-order components aren’t just for authentication systems, but can be used to solve a variety of problems. Be on the lookout for other places where you could use higher-order components, you might be surprised on their versatility.
Maybe you want to A/B test some components?
// our higher-order component
function abTest(ComponentA, ComponentB) {
return new class extends Component {
private isInGroupA;
constructor(props) {
super(props);
this.isInGroupA = testingFramework.isInGroupA();
}
render() {
return this.isInGroupA ?
<ComponentA {...this.props}/> :
<ComponentB {...this.props}/>;
}
};
}
// ...
// create a callToAction component using our higher-order component
const callToAction = abTest(GreenCTA, BlueCTA);
// ...
// all we need to render is our callToAction component
render() {
return <callToAction onClick={this._signUp} />;
}
Maybe you are collecting metrics, or debugging, and want to know when a component is rendered?
function logger(WrappedComponent, metricKey) {
return new class extends Component {
render() {
metrics.report(metricKey);
return <WrappedComponent {...this.props}/>;
}
};
}
// ...
const loggedComponent = logger(MyPage, 'my-page');
Maybe you want to automatically inject properties into some components?
function UserAware(WrappedComponent) {
return new class extends Component {
render() {
const user = auth.getUser();
return <WrappedComponent {...{...props, user}}/>;
}
};
}
// ...
const AccountPage = UserAware(Account);
Or, maybe you want to do all of that!
const AccountPage = UserAware(Authorized(ABTest(logger(AccountOriginal, "account.original"), logger(AccountNew, "account.new"))));
You can see that this design pattern packs a lot of punch. Higher-order components may not be the silver bullet solution to all your problems, but they deserve a significant place in your design pattern tool belt. How do you know if your problem is a good a fit for this pattern? If you are adding the same code, especially code to your render method, to multiple components, this pattern might be a good fit.
If you are interested in learning more about higher-order components, read about them out in the React Docs.