When to use higher-order components in React

When to use higher-order components in React

·

5 min read

If you've written React code recently, you've probably used some official React hooks like useState or useEffect. In this post, we'll look at what higher-order components are and when it can help us eliminate some extra boilerplate vs hooks.

Analytics example

For a lot of products, you'll want to add some sort of tracking of key events. What pages are my users visiting, where are my users spending the most time, etc. Let's say we have some function recordEvent(eventName) which will save the event to our analytics store.

Here's a simple example page where we're recording an event on the user's initial page load and every 30 seconds with recordEvent:

const HelpPage = () => {
    // On initial load, record an event
    useEffect(() => {
        recordEvent("VISIT_HELP_PAGE")
    }, [])

    // Every 30 seconds, record another event if the page itself is not hidden 
    useEffect(() => {
        const interval = setInterval(() => {
            if (!document.hidden) {
                recordEvent("STILL_ON_HELP_PAGE")
            }
        }, 30000);
        return () => clearInterval(interval);
    }, []);

    return <div>{/* Render the page */}</div>
}

export default HelpPage

If we want to re-use this functionality across other components, we can make a custom hook:

// useAnalytics.js
function useAnalytics(initialEventName, periodicEventName) {
    // On initial load, record an event
    useEffect(() => {
        recordEvent(initialEventName)
    }, [])

    // Every 30 seconds, record another event if the page itself is not hidden 
    useEffect(() => {
        const interval = setInterval(() => {
            if (!document.hidden) {
                recordEvent(periodicEventName)
            }
        }, 30000);
        return () => clearInterval(interval);
    }, []);
}

// HelpPage.js
const HelpPage = () => {
    useAnalytics("VISIT_HELP_PAGE", "STILL_ON_HELP_PAGE")
    return <div>{/* Render the page */}</div>
}

export default HelpPage;

Another option, is to use a higher-order component. The idea behind a higher-order component is we have a function that takes in a component and returns a new component. In our analytics example, we'll take in our HelpPage component, and return a new component with our two useEffect calls at the top:

function withAnalytics(WrappedComponent, initialEventName, periodicEventName) {
    const ComponentWithAnalytics = (props) => {
        // On initial load, record an event
        useEffect(() => {
            recordEvent(initialEventName)
        }, [])
        // ...etc

        // Make sure to pass the props along 
        return <WrappedComponent {...props} />
    }

    // Convention: Wrap the display name
    ComponentWithAnalytics.displayName = `WithAnalytics(${getDisplayName(WrappedComponent)})`;
    return ComponentWithAnalytics
}

function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

This allows us to write:

const HelpPage = () => {
    return <div>{/* Render the page */}</div>
}
const HelpPageWithAnalytics = withAnalytics(HelpPage, "VISIT_HELP_PAGE", "STILL_ON_HELP_PAGE");
export default HelpPageWithAnalytics

Comparing these two code snippets, the final result has a similar amount of code. However, higher-order components come with some additional things to worry about like:

There are libraries like hoist-non-react-statics which help reduce some of these pain points, but in this example, I'd prefer to just use the hook. Let's look at some examples where a higher-order component is more appealing.

Creating a higher-order component around useSWR

The biggest advantage of a higher-order component is that it can return whatever component it wants. If you want to return a loading spinner or error message instead of the wrapped component, you can do that.

Another advantage is it can select which props (or create new props) to be passed to the wrapped component. To see these in action, let's build a higher-order component around useSWR.

Here's a minimal example from SWR's website, where we fetch user information from an API and render it:

import useSWR from 'swr'

function Profile() {
    const { data, error } = useSWR('/api/user', fetcher)

    if (error) return <div>failed to load</div>
    if (!data) return <div>loading...</div>
    return <div>hello {data.name}!</div>
}

Now, let's look at how this code could look with a higher-order component:

function ProfileInner({data}) {
    return <div>hello {data.name}!</div>
}
const Profile = withSWR(ProfileInner, '/api/user')

Without showing withSWR, what is it taking care of for us? The most obvious thing is that it must be making the call to useSWR for us. We also no longer have an error, which means it is handling displaying the error. Similarly, we don't seem to have a loading message, so it must take care of that too.

By hiding the error and loading in withSWR, it does two things for us:

  1. We only have to worry about displaying the result in the successful case
  2. We have no control over how errors and loading messages look for Profile

We can fix 2 by providing ways to display an error or a loading message, like so:

function ProfileInner({data}) {
    return <div>hello {data.name}!</div>
}
const Profile = withSWR(ProfileInner, '/api/user', {
    loadingComponent: <div>loading...</div>,
    errorComponent: <div>failed to load</div>
})

and this is fine, but we are back to taking on the complexities associated with a higher-order component, and we're still writing a similar amount of code to the hook case.

When would we choose a higher-order component over a hook?

Personally, I think one of the strongest cases for using a higher-order component is when you have a consistent loading or error component across your application. withSWR above is really appealing if we use the same loading spinner everywhere. It can save a lot of boilerplate from the hook cases, so you don't have to keep writing if statements after hooks.

Additionally, class components do not support hooks. If you are using class components and want to use a hook, your best option is to create a functional higher-order component which calls the hook and passes down props to your class component.

Practically speaking, I tend to make hooks first. If I find myself writing a lot of extra boilerplate code on top of the hook, then I'll make a higher-order component, and often that component will use the hook itself!