Higher-Order Components In React Hooks Era - Developer Way

Is it true that React hooks made higher-order components obsolete? And the only use case for those is to be a remnant of the past in some existential legacy corners of our apps? And what is a higher-order component anyway? Why did we need them in the first place?

Answering those questions and building a case that higher-order components are still useful even in modern apps for certain types of tasks.

But let's start from the beginning.

What is a higher-order component?

According to React docs, it’s an advanced technique to re-use components logic that is used for cross-cutting concerns, if that description means anything to you (for me not so much 🙂).

In English, it’s just a function, that accepts a component as one of its arguments, messes with it, and then returns back its changed version. The simplest variant of it, that does nothing, is this:

// accept a Component as an argumentconstwithSomeLogic=(Component)=>{// do something// return a component that renders the component from the argumentreturn(props)=><Component{...props}/>;};

The key here is the return part of the function - it’s just a component, like any other component. And similar to the render props pattern, we need to pass props to the returned component, otherwise, they will be swallowed.

And then, when it’s time to use it, it would look like this:

constButton=({ onClick })=><buttononClick={func}>Button</button>;constButtonWithSomeLogic=withSomeLogic(Button);

You pass your Button component to the function, and it returns the new Button, which includes whatever logic is defined in the higher-order component. And then this button can be used as any other button:

constSomePage=()=>{return(<><Button/><ButtonWithSomeLogic/></>);};

If we want to create a mental map of what goes where it could look something like this:

Play around with those examples in codesandbox.

Before the introduction of hooks, higher-order components were widely used for accessing context and any external data subscriptions. Redux connect or react-router’s withRouter functions are higher-order components: they accept a component, inject some props into it, and return it back.

// location is injected by the withRouter higher-order component// would you guessed that by the look at this component alone?constSomeComponent=({location})=>{return<>{location}</>;};constComponentWithRouter=withRouter(SomeComponent);

As you can see, higher-order components are quite complicated to write and to understand. So when the hooks were introduced, no wonder everyone switched to them.

Now, instead of creating complicated mental maps of which prop goes where and trying to figure out how location ended up in props, we can just write:

constSomeComponent=()=>{// we see immediately where location is coming fromconst{location}=useRouter();return<>{location}</>;};

Everything that is happening in the component can be read from top to bottom and the source of all the data is obvious, which significantly simplifies debugging and development.

And while hooks probably replaced 90% of shared logic concerns and 100% of use-cases for accessing context, there are still at least three types of functionality, where higher-order components could be useful.

Let’s take a look at those.

First: enhancing callbacks and React lifecycle events

Imagine you need to send some sort of advanced logging on some callbacks. When you click a button, for example, you want to send some logging events with some data. How would you do it with hooks? You’d probably have a Button component with an onClick callback:

typeButtonProps={onClick:()=>void; children:ReactNode;}constButton=({ onClick }:ButtonProps)=>{return<buttononClick={onClick}>{children}</button>}

And then on the consumer side, you’d hook into that callback and send logging event there:

constSomePage=()=>{const log =useLoggingSystem();constonClick=()=>{log('Button was clicked');};return<ButtononClick={onClick}>Click here</Button>;};

And that is fine if you want to fire an event or two. But what if you want your logging events to be consistently fired across your entire app, whenever the button is clicked? We probably can bake it into the Button component itself.

constButton=({ onClick }:ButtonProps)=>{const log =useLoggingSystem();constonButtonClick=()=>{log('Button was clicked')onClick();}return<buttononClick={onButtonClick}>{children}</button>}

But then what? For proper logs you’d have to send some sort of data as well. We surely can extend the Button component with some loggingData props and pass it down:

constButton=({ onClick, loggingData }:ButtonProps)=>{constonButtonClick=()=>{log('Button was clicked', loggingData)onClick();}return<buttononClick={onButtonClick}>{children}</button>}

But what if you want to fire the same events when the click has happened on other components? Button is usually not the only thing people can click on in our apps. What if I want to add the same logging to a ListItem component? Copy-paste exactly the same logic there?

constListItem=({ onClick, loggingData }:ListItemProps)=>{constonListItemClick=()=>{log('List item was clicked', loggingData)onClick();}return<ItemonClick={onListItemClick}>{children}</Item>}

Too much copy-pasta and prone to errors and someone forgetting to change something in my taste.

What I want, essentially, is to encapsulate the logic of “something triggered onClick callback - send some logging events” somewhere, and then just re-used it in any component I want, without changing the code of those components in any way.

And this is the first use case where the hooks are no use, but higher-order components could come in handy.

Higher-order component to enhance onClick callback

Instead of copy-pasting the “click happened → log data” logic everywhere, I can just create a withLoggingOnClick function, that:

  • accepts a component as an argument
  • intercepts its onClick callback
  • sends the data that I need to the whatever external framework is used for logging
  • returns the component with onClick callback intact for further use

It would look something like this:

typeBase={onClick:()=>void};// just a function that accepts Component as an argumentexportconst withLoggingOnClick =<TPropsextendsBase>(Component: ComponentType<TProps>) => {return(props:TProps)=>{constonClick=()=>{console.log('Log on click something');// don't forget to call onClick that is coming from props!// we're overriding it below props.onClick();};// return original component with all the props// and overriding onClick with our own callbackreturn<Component{...props}onClick={onClick}/>;};};

And now I can just add it to any component that I want. I can have a Button with logging baked in:

exportconstButtonWithLoggingOnClick=withLoggingOnClick(SimpleButton);

Or use it in the list item:

exportconstListItemWithLoggingOnClick=withLoggingOnClick(ListItem);

Or any other component that has onClick callback that I want to track. Without a single line of code changed in either Button or ListItem components!

Adding data to the higher-order component

Now, what’s left to do, is to add some data from the outside to the logging function. And considering that higher-order component is nothing more than just a function, we can do that easily. Just need to add some other arguments to the function, that’s it:

typeBase={onClick:()=>void};exportconst withLoggingOnClickWithParams =<TPropsextendsBase>( Component: ComponentType<TProps>, // adding some params as a second argument to the function params: { text:string},) => {return(props:TProps)=>{constonClick=()=>{// accessing params that we passed as an argument here// everything else stays the sameconsole.log('Log on click: ', params.text); props.onClick();};return<Component{...props}onClick={onClick}/>;};};

And now, when we wrap our button with higher-order component, we can pass the text that we want to log:

constButtonWithLoggingOnClickWithParams=withLoggingOnClickWithParams(SimpleButton,{ text:'button component'});

On the consumer side, we’d just use this button as a normal button component, without worrying about the logging text:

constPage=()=>{return<ButtonWithLoggingOnClickWithParamsonClick={onClickCallback}>Click me</ButtonWithLoggingOnClickWithParams>;};

But what if we actually want to worry about this text? What if we want to send different texts in different contexts of where the button is used? We wouldn’t want to create one million wrapped buttons for every use case.

Also very easy to solve: instead of passing that text as function’s argument, we can inject it as a prop to the resulting button. The code would look like this:

typeBase={onClick:()=>void};exportconst withLoggingOnClickWithProps =<TPropsextendsBase>(Component: ComponentType<TProps>) => {// our returned component will now have additional logText propreturn(props:TProps&{ logText:string})=>{constonClick=()=>{// accessing it here, as any other propsconsole.log('Log on click: ', props.logText); props.onClick();};return<Component{...props}onClick={onClick}/>;};};

And then use it like this:

constPage=()=>{return(<ButtonWithLoggingOnClickWithPropsonClick={onClickCallback}logText="this is Page button"> Click me</ButtonWithLoggingOnClickWithProps>);};

See the codesandbox with all the examples.

Sending data on mount instead of click

We are not limited to clicks and callbacks here. Remember, those are just components, we can do whatever we want and need 🙂 We can use everything React has to offer. For example, we can send those logging events when a component is mounted:

exportconst withLoggingOnMount =<TPropsextendsunknown>(Component: ComponentType<TProps>) => {return(props:TProps)=>{// no more overriding onClick, just adding normal useEffectuseEffect(()=>{console.log('log on mount');},[]);// just passing props intactreturn<Component{...props}/>;};};

And exactly the same story as with onClick for adding data via arguments or props. Not going to copy-paste it here, see it in the codesandbox.

We can even go wild and combine all of those higher-order components:

exportconstSuperButton=withLoggingOnClick(withLoggingOnClickWithParams(withLoggingOnClickWithProps(withLoggingOnMount(withLoggingOnMountWithParams(withLoggingOnMountWithProps(SimpleButton),{ text:'button component'})),),{ text:'button component'},),);

We shouldn’t do this of course though 😅 If something is possible, it doesn’t always mean it’s a good idea. Imagine trying to trace which props come from where, when debugging time comes. If we really need to combine a few higher-order components into one, we can be at least a bit more specific about it:

constButtonWithLoggingOnClick=withLoggingOnClick(SimpleButton);constButtonWithLoggingOnClickAndMount=withLoggingOnMount(ButtonWithLoggingOnClick);// etc

Second: intercepting DOM events

Another very useful application of higher-order components is intercepting various DOM events. Imagine, for example, you implement some sort of keyboard shortcuts functionality on your page. When specific keys are pressed, you want to do various things, like open dialogs, creating issues, etc. You’d probably add an event listener to window for something like this:

useEffect(()=>{constkeyPressListener=(event)=>{// do stuff};window.addEventListener('keypress', keyPressListener);return()=>window.removeEventListener('keypress', keyPressListener);},[]);

And then, you have various parts of your app, like modal dialogs, dropdown menus, drawers, etc, where you want to block that global listener while the dialog is open. If it was just one dialog, you can manually add onKeyPress to the dialog itself and there do event.stopPropagation() for that:

exportconstModal=({ onClose }:ModalProps)=>{constonKeyPress=(event)=> event.stopPropagation();return<divonKeyPress={onKeyPress}>...// dialog code</div>;};

But the same story as with onClick logging - what if you have multiple components where you want to see this logic?

What we can do here, is again implement a higher-order component. This time it will accept a component, wrap it in a div with onKeyPress callback attached, and return the component unchanged.

exportconst withSupressKeyPress =<TPropsextendsunknown>(Component: ComponentType<TProps>) => {return(props:TProps)=>{constonKeyPress=(event)=>{ event.stopPropagation();};return(<divonKeyPress={onKeyPress}><Component{...props}/></div>);};};

That is it! Now we can just use it everywhere:

constModalWithSupressedKeyPress=withSupressKeyPress(Modal);constDropdownWithSupressedKeyPress=withSupressKeyPress(Dropdown);// etc

One Important thing to note here: focus management. In order for the above code to actually work, you need to make sure that your dialog-type components move focus to the opened part when they are open. But this is a completely different conversation on focus management, maybe next time.

For the purpose of the example, we can just manually include auto-focus in the modal itself:

constModal=()=>{const ref =useRef<HTMLDivElement>();useEffect(()=>{// when modal is mounted, focus the element to which the ref is attachedif(ref.current) ref.current.focus();},[]);// adding tabIndex and ref to the div, so now it's focusablereturn<divtabIndex={1}ref={ref}> <!-- modal code --></div>}

Play around with it in the codesandbox.

Third: context selectors

The final and very interesting use case for higher-order components: selectors-like functionality for React context. As we know, when context value changes, it will cause re-renders of all context consumers, regardless of whether their particular part of the state was changed or not. (And if you didn’t know about it, here’s the article for you: How to write performant React apps with Context).

Let’s implement some context and form first, before jumping into higher-order components.

We’ll have Context with id and name and API to change those:

typeContext={ id:string; name:string;setId:(val:string)=>void;setName:(val:string)=>void;};const defaultValue ={ id:'FormId', name:'',setId:()=>undefined,setName:()=>undefined,};constFormContext=createContext<Context>(defaultValue);exportconstuseFormContext=()=>useContext(FormContext);exportconstFormProvider=({ children }:{ children:ReactNode})=>{const[state, setState]=useState(defaultValue);const value =useMemo(()=>{return{ id: state.id, name: state.name,setId:(id:string)=>setState({...state, id }),setName:(name:string)=>setState({...state, name }),};},[state]);return<FormContext.Providervalue={value}>{children}</FormContext.Provider>;};

And then some form with Name and Countries components

constForm=()=>{return(<formcss={pageCss}><Name/><Countries/></form>);};exportconstPage=()=>{return(<FormProvider><Form/></FormProvider>);};

Where in Name component we’ll have an input that changes the value of Context, and Countries just use the id of the form to fetch the list of countries (not going to implement the actual fetch, not important for the example:

constCountries=()=>{// using only id from context hereconst{ id }=useFormContext();console.log("Countries re-render");return(<div><h3>List on countries for form: {id}</h3><ul><li>Australia</li><li>USA</li> <!-- etc --></ul></div>);}; constName=()=>{// using name and changing it hereconst{ name, setName }=useFormContext();return<inputonChange={(event)=>setName(event.target.value)}value={name}/>;};

Now, every time we type something in the name input field, we’ll update the context value, which will cause re-render of all components that use context, including Countries. And this can’t be solved by extracting this value into a hook and memoising it: hooks always re-render (Why custom react hooks could destroy your app performance).

There are other ways to deal with it of course, if this behaviour causes performance concerns, like memoising parts of render tree or splitting Context into different providers (see those articles that describe those techniques: How to write performant React apps with Context and How to write performant React code: rules, patterns, do's and don'ts).

But big disadvantage of all the techniques above, is that they are not shareable and need to be implemented on a case-by-case basis. Wouldn’t it be nice, if we had some select-like functionality, that we can use to extract this id value safely in any component, without significant refactorings and useMemo all over the app?

Interestingly enough, we can implement something like this with higher-order components. And the reason for this is that components have one thing that hooks don’t give us: they can memoise things and stop the chain of re-renders going down to children. Basically, this will give us what we want:

exportconst withFormIdSelector =<TPropsextendsunknown>( Component: ComponentType<TProps & { formId:string}>) => {constMemoisedComponent=React.memo(Component)asComponentType<TProps&{ formId:string}>;return(props:TProps)=>{const{ id }=useFormContext();return<MemoisedComponent{...props}formId={id}/>;};};

and then we can just create CountriesWithFormIdSelector component:

// formId prop here is injected by the higher-order component belowconstCountriesWithFormId=({ formId }:{ formId:string})=>{console.log("Countries with selector re-render");return(<--codeisthesameasbefore--> );};const CountriesWithFormIdSelector = withFormIdSelector(CountriesWithFormId);

And use it in our form:

constForm=()=>{return(<formcss={pageCss}><Name/><CountriesWithFormIdSelector/></form>);};

Check it out in the codesandbox. Pay special attention of the console output when typing in the input - CountriesWithFormIdSelector component doesn’t re-render!

Generic React context selector

withFormIdSelector is fun and could work for small context-based apps. But wouldn’t it be nice to have it as something generic? So that we don’t have to implement a custom selector for every state property.

No problem when some creative hackery is involved! Check it out, selector itself:

exportconst withContextSelector =<TPropsextendsunknown,TValueextendsunknown>(Component:ComponentType<TProps&Record<string,TValue>>, selectors:Record<string,(data:Context)=>TValue>,):ComponentType<Record<string,TValue>>=>{// memoising component generally for every propconstMemoisedComponent=React.memo(Component)asComponentType<Record<string,TValue>>;return(props:TProps&Record<string,TValue>)=>{// extracting everything from contextconst data =useFormContext();// mapping keys that are coming from "selectors" argument// to data from contextconst contextProps =Object.keys(selectors).reduce((acc, key)=>{ acc[key]= selectors[key](data);return acc;},{});// spreading all props to the memoised componentreturn<MemoisedComponent{...props}{...contextProps}/>;};};

and then use it with components:

// props are injected by the higher order component belowconstCountriesWithFormId=({ formId, countryName }:{ formId:string; countryName:string})=>{console.log('Countries with selector re-render');return(<div><h3>List of countries for form: {formId}</h3> Selected country: {countryName}<ul><li>Australia</li><li>USA</li></ul></div>);};// mapping props to selector functionsconstCountriesWithFormIdSelector=withContextSelector(CountriesWithFormId,{formId:(data)=> data.id,countryName:(data)=> data.country,});

And that’s it! we basically implemented mini-Redux on context, even with proper mapStateToProps functionality 🙂 Check it out in the codesandbox.

That is it for today! Hope higher-order components are not some terrifying legacy goblins now, but something you can put to good use even in modern apps. Let’s re-cap the use cases for those:

  • to enhance callbacks and React lifecycle events with additional functionality, like sending logging or analytics events
  • to intercept DOM events, like blocking global keyboard shortcuts when a modal dialog is open
  • to extract a piece of Context without causing unnecessary re-renders in the component

May the peace and love be with you ✌🏼

Tag » What Are Higher Order Components