Stacked Higher Order Components (HOC) done the right way

Update - This is still relevant with hooks instead make the HOC return a functional component rather than a class.

If you are a react developer and are not familiar with higher order components (HOC) then you are in for a treat. if you are familiar you might have stumbled about the difficulty of stacking HOCs, specially when some functionality requires a reference to the original component.

Let me show you the code (with additional comments) of which I am really proud of, hopefully it illustrates the power of HOCs. It is for react-native. I am proud of it because it allows me to toggle functionality, debug, and reuse the HOC code easily (i actually do).

import { flow } from "lodash";

import handleKeyboard from "./handleKeyboard";
import restrictWebviewAccess from "./restrictWebviewAccess";
import fillWidth from "./fillWidth";
import sessionEvents from "./sessionEvents";
import browserFunctionality from "./browserFunctionality";
import reloadOnReactivate from "./reloadOnReactivate";
import addSplash from "./addSplash";

// Base Component
import WebView from "./WebView";

// Compose the new webview
export default flow([
  restrictWebviewAccess, // block access to external sites
  browserFunctionality, // handle android back button
  reloadOnReactivate, // reload after the app has been inactive
  handleKeyboard, // make android not hide webview input
  fillWidth, // make webview full width
  sessionEvents, // add login, logout session events
  addSplash, // add splash to webview
])(WebView);

First lets explore what HOCs are. What are HOCs?

They are functions which accept a Child Component and returns a new Component which renders the Child Component with additional functionality… Well that was a lot to digest, but it is actually really simple, lets look at an example.

Lets make a higher order component that adds a label (prop labelText) to an input.

First we make a base component. The reason is ‘input’ is not a variable declared, so we must make a dumb react component in its place.

// Base Component, returns an input with all the props we give it
const Input = (props) => <input {...props} />;

Now lets make the higher order component. HOC is a function that returns a new component.

// HOC
const addLabel = (Child) => {
  return ({ labelText, ...props }) => (
   <div>
     <label>{labelText}</label>
     <Child {...props}/>
   </div>
  );
}

Now lets use it

const InputWithLabel = addLabel(Input);// then we can render it like this anywhere we want

That is it! we have an component that is an input with a label. Image the power of this to add and remove functionality! It allows you to separate code and compose components! I like to think of these as modules because they add functionality to a simple component. The Challenge!

I was working with react-native (dont get intimidated if you have never worked with it, its basically the same). I started out using WebView, like a browser component, and i started to use its lifecycle functions to detect when it was loading, block it from going places where i did not want the user to go to, and the code got REALLY MESSY, REALLY FAST!

The solutions was to develop it with HOC’s

For the sake of this article i will show you with an input components the problems i ran into. Problem #1: Tapping into its event functions

When you have multiple HOC’s that use the same event function, what do you do? If you just send in your event handling function you might be depriving another HOC of handling the same event.

The solution is simple. ALWAYS call the prop function. there are 2 ways to do this, i will show you.

// SOLUTION 1: Check to see if you have this.props.onChange (or any other event function)
const firstHOC = (Child) => {
  return class FirstHOC extends component {
    onChange = (event) => {
      // Do whatever this higher order component does here      // Check if another event function should be triggered
      if (this.props.onChange) {
        this.props.onChange(event);
      }
    };
    render() {
      return <Child {...this.props} onChange={this.onChange} />;
    }
  };
};

// SOLUTION 2: assign a default function as a prop. i like this a lot more
const secondHOC = (Child) => {
  return class SecondHOC extends component {
    static defaultProps = {
      onChange: () => null,
    };

    onChange = (event) => {
      // Do whatever this higher order component does here      // No need to check
      this.props.onChange(event);
    };

    render() {
      return <Child {...this.props} onChange={this.onChange} />;
    }
  };
};

Essentially all onChange functions will be triggered in a sort of recursive manner.

Fairly easy, this is almost trivial but I thought I should cover this. The next problem is the really juicy one. The reason why I am writing this article. Problem #2: HOC that require references to work

If you are familiar with react you might be cringing but bear with me, sometimes it is required. For example if you need to trigger the reload method on a WebView.

So the issue is that if you stack HOC’s only the first one will have a ref of the initial component.

Here is the example i will use to illustrate the problem and to illustrate the solution. We are going to make an Input that will autofocus onBlur, setSelection to the end onChange, and autoFocus on mounting. We will do this all with HOC so that we can easy remove and edit functionality if we wanted to. By the way this might not be the right thing to do but the example is meant to illustrate the problem in a somewhat trivial scenario.

So lets build all the HOCs

// Base component
const Input = (props) => <input {...props} />; // AutoFocus on mount HOC

const autoFocus = (Child) => {
  return class AutoFocus extends component {
    // Autofocus on mount
    componentDidMount = () => {
      this.input.focus();
    };

    getReference = (ref) => {
      this.input = ref;
    };

    render() {
      return <Child {...this.props} ref={this.getReference} />;
    }
  };
};

// AutoFocus on Blur HOC
const autoFocusOnBlur = (Child) => {
  return class AutoFocusOnBlur extends component {
    static defaultProps = {
      onBlur: () => null,
    }; // Autofocus on blur
    onBlur = (event) => {
      this.input.focus();
      this.props.onBlur(event);
    };

    getReference = (ref) => {
      this.input = ref;
    };

    render() {
      return (
        <Child {...this.props} onBlur={this.onBlur} ref={this.getReference} />
      );
    }
  };
};

// onChange put cursor at end
const setSelectionAtEndOnChange = (Child) => {
  return class SetSelectionAtEndOnChange extends component {
    static defaultProps = {
      onChange: () => null,
      value: "",
    }; // put the cursor at the end onChange
    onChange = (event) => {
      const endOfText = this.props.value.length;
      this.input.setSelection(endOfText, endOfText);
      this.props.onChange(event);
    };

    getReference = (ref) => {
      this.input = ref;
    };

    render() {
      return (
        <Child
          {...this.props}
          onChange={this.onChange}
          ref={this.getReference}
        />
      );
    }
  };
};

Alright!! Wow that was a lot… but its fairly simple things. You might have not seen the issue yet so lets use these and see what the problem is.

let FrankensteinInput = autoFocus(Input);
FrankensteinInput = autoFocusOnBlur(Input);
FrankensteinInput = setSelectionAtEndOnChange(Input);

So now we have our input will all the functionality that we want… except it does not work. But why wouldn’t it work? First autoFocus will have a reference of Input but not of the actual input. But even if it did have a reference of input, every other HOC will have a reference of the previous HOC.

The SOLUTION is to make a function that gets called recursively so that every HOC has a reference to the original component this is a modification to every HOC and the original base component.

So lets change our base component.

// Original
const Input = (props) => <input {...props} />;//Modified
const Input = ({ getReference, ...props}) => <input {...props} ref={getReference} />;

Now I will show you the change required for one of the previous HOC because the same change will need to happen to all of them. Note the getPreference defaultProp and then the getReference prop we pass.

// Original
const autoFocus = (Child) => {
  return class AutoFocus extends component {
    // Autofocus on mount
    componentDidMount = () => {
      this.input.focus();
    };

    getReference = (ref) => {
      this.input = ref;
    };

    render() {
      return <Child {...this.props} ref={this.getReference} />;
    }
  };
};

// Modified
const autoFocus = (Child) => {
  return class AutoFocus extends component {
    static defaultProps = {
      getReference: () => false,
    };

    // Autofocus on mount
    componentDidMount = () => {
      this.input.focus();
    };

    getReference = (ref) => {
      this.input = ref;

      // Call so that other HOC's get the reference
      this.props.getReference(ref);
    };
    render() {
      return <Child {...this.props} getReference={this.getReference} />;
    }
  };
};

Now it will all work.

Let me know what you think in the comments below!