Higher order components – HOCs – are one of the best ways to improve your React code. When you see a lot of components sharing similar code, it's time for a HOC.
A HOC used as a decorator can turn a 36-line component like this:
@inject("configStore")
@observer
class Toolbox extends Component {
constructor(props) {
super(props);
autorun(this._render.bind(this));
}
componentDidUpdate() {
this._render();
}
componentDidMount() {
this._render();
}
componentWillUnmount() {
this._cleanup();
}
_init() {
this.backbone = new ToolboxView({
currentUser: new Backbone.Model(toJS(this.props.configStore.currentUser)),
pages: [],
});
}
_render() {
this._cleanup();
this._init();
this.backbone.setElement(this.refs.anchor).render();
}
_cleanup() {
if (this.backbone) {
this.backbone.undelegateEvents();
}
}
render() {
return <div ref="anchor"></div>;
}
}
Into an 11-line beauty like this:
@inject("configStore")
@observer
@backbone(ToolboxView)
class Toolbox extends Component {
backboneProps = {
currentUser: new Backbone.Model(toJS(this.props.configStore.currentUser)),
pages: [],
};
render() {
return this.anchor;
}
}
This is a Backbone integration HOC based on my approach to integrating React, MobX, and Backbone from a few months ago. It takes away all the integration boilerplate and lets you focus on the important stuff – setting data for the old Backbone View and potential rendering embelishments.
The same approach should work with any component-based library like Preact, Inferno, and Vue, but I haven't tried yet.
So how does this work? How did a HOC replace 25 lines of JavaScript with a single @backbone(ToolboxView)
decorator? Let me show you.
How HOCses improve your code
The first step is to take everything generic out of your component and put it into a new one. This will be what your HOC does.
If you can't tell what's generic, wait until you have two components that look similar. Any boilerplate they share is a good candidate for a HOC.
In our example that boilerplate is:
- the constructor that sets up
autorun
- functions keeping Backbone and React render in sync
- all the Backbone rendering things
- an anchor element for Backbone to hook into
Soooo… everything. ?
We're leaving React's render()
method out of the HOC to give consumers better flexibility, and we're adding the ability to set Backbone params. Those are important.
Step 1: the core of a HOC
The core of our HOC is a React (or Preact or Inferno) component:
class Backbone extends WrappedComponent {
constructor(props) {
super(props);
autorun(this._render.bind(this));
}
componentDidUpdate() {
if (super.componentDidUpdate) {
super.componentDidUpdate();
}
this._render();
}
componentDidMount() {
this._render();
if (super.componentDidMount) {
super.componentDidMount();
}
}
componentWillUnmount() {
if (super.componentWillUnmount) {
super.componentWillUnmount();
}
this._cleanup();
}
_init() {
this.backbone = new BackboneView(
Object.assign({},
this.props,
this.backboneProps
));
}
_render() {
this._cleanup();
this._init();
this.backbone.setElement(this.refs.anchor).render();
}
_cleanup() {
if (this.backbone) {
this.backbone.undelegateEvents();
}
}
get anchor() {
return (<div ref="anchor">)
}
}
</div>
Looks almost the same as our original implementation.
constructor
sets up autorun
so our component can react to MobX store changes. This automatically turns our Backbone'd components into observers, but I like to include the @observer
call when using them anyway. Helps with readability :)
The componentDidMount
, componentDidUpdate
, and componentWillUnmount
methods ensure our Backbone View gets rendered, re-rendered, and cleaned up when appropriate. Each method includes a hook for users of our HOC to add their own lifecycle logic. I haven't really had a use for this yet, but I think it's a good idea to have.
_init
, _render
, and _cleanup
handle the actual Backbone stuff. They're helper methods that make it easier to reuse bits in different React lifecycle methods.
get anchor
is like a render()
method but not. We could render here, but that would reduce flexibility. Instead, we give consumers an easy way to render the Backbone anchor point wherever.
Step 2: Making it a HOC
To turn this component into a real HOC, we have to wrap it in a function. You could call this function an class factory because that's what it is. It creates classes.
function (WrappedComponent) {
return // code from above
}
Yep, a function that takes a component to be wrapped and returns a class extending said component. That really is it.
Congratulations, you have made a HOC.
Caveat: we use super
when to invoke a method on the parent class – the wrapped component – and this
when we access instance properties even if they are defined on the parent. Both your wrapped component and your HOC core component share the same this
reference.
Step 3: Adding more arguments
Now here's a tricky part. Adding more arguments – like which Backbone View to wrap with – requires currying. At least it does if you want to use your HOC as a decorator like I did.
Currying means that instead of having functions with multiple arguments, we create a composition of single-argument functions. I'm not entirely certain why currying is all the rage, but it makes many approaches to functional programming easier to use.
Our HOC then looks like this:
function backbone(BackboneView) {
return function (WrappedComponent) {
return class Backbone extens WrappedComponent {
// your HOC core
}
}
}
Now the logic inside your HOC can use BackboneView
to decide which component to use. This means that you don't need a new HOC for every Backbone View you want to make available in React. Wonderful.
Step 4: Using it
With all that done, your HOC is ready to use. In our case, we use it for rendering old Backbone Views in React code.
Using it looks like this:
@inject("configStore")
@observer
@backbone(ToolboxView)
class Toolbox extends Component {
backboneProps = {
currentUser: new Backbone.Model(toJS(this.props.configStore.currentUser)),
pages: [],
};
render() {
return this.anchor;
}
}
backboneProps
lets us define parameters fed into the Backbone View – the HOC accesses them as this.backboneProps
.
Inside render()
, we use this.anchor
anywhere we want to place the anchor point for our Backbone View. In this case we're rendering just that, but in others we might want to wrap it with useful markup that makes it look better in its new context.
Conclusion
HOCs are great, and I wish I started using them earlier. There's definitely going to be a chapter about HOCses in the new version of React+D3. Some prime targets for this stuff in there.
And yes, you can use them without decorators. It's just not as pretty. Something like this:
class MyThing extends Component {
// stuff
}
export backbone(OldView)(MyThing);
Now how would I make them work with functional stateless components? ? Should be just function composition, right?
Continue reading about Using HOCs to DRY up your code
Semantically similar articles hand-picked by GPT-4
- Backbone → React: Handling state with MobX
- How to use MobX with create-react-app
- Backbone → React: it's a people problem after all ?
- How to structure your MobX app for the real world
- Easy D3 blackbox components with React hooks
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️