Swizec Teller - a geek with a hatswizec.com

Senior Mindset Book

Get promoted, earn a bigger salary, work for top companies

Senior Engineer Mindset cover
Learn more

    Using HOCs to DRY up your code

    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;
      }
    }
    
    1282786204310

    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?

    Published on December 16th, 2016 in Front End, react, Technical

    Did you enjoy this article?

    Continue reading about Using HOCs to DRY up your code

    Semantically similar articles hand-picked by GPT-4

    Senior Mindset Book

    Get promoted, earn a bigger salary, work for top companies

    Learn more

    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 ❤️

    Created by Swizec with ❤️