Why we still use React HOCs

Why we still use React HOCs

If you’ve integrated with our React library, @propelauth/react, you’ve probably seen that we offer a pretty standard React hook useAuthInfo. It returns a few important pieces of information like:

  • isLoggedIn - whether or not the user is logged in, as you probably already knew
  • user - the user’s information
  • orgHelper and accessHelper - convenience objects for access organization/RBAC information for this user
  • loading - the field that I hate

I don’t just hate our loading field, I hate all loading fields.

It’s obviously important, we want to display something to the user to indicate that we’re, you know, doing something. But it feels like I end up writing code like this too often:

const Component = () => {
    const hookResponse = useHookWithLoadingState()
    if (hookResponse.loading) {
        return <Loading />
    }

    // Now do the thing I wanted to do
}

There are ways to work around a loading state - you could use SSR and just inject the information into your components. But for a standard, client-side React application, you likely will run into loading states often.

Just use two components

One approach you can take is just split the work up:

const ComponentThatDoesTheInitialWork = () => {
    const hookResponse = useHookWithLoadingState()
    if (hookResponse.loading) {
        return <Loading />
    }

    return <Component hookResponseData={hookResponse.data} />
}

const Component = ({hookResponseData}) => {
    // Now do the thing I wanted to do
}

This is technically more code, but we have clear, separate responsibilities. The first component will check the loading state, and the second, inner component just has to worry about using the information.

For a single component, this is totally reasonable. What happens, however, if we want to use this hook all over our codebase? Let’s see how we can do better with Higher Order Components.

Using Higher Order Components to avoid loading state

Let’s re-write our first component to look like this:

// This is an incomplete example to illustrate a point, don't use it directly

const withHookWithLoadingState = (InnerComponent) => {
    // Make a component which passes hookResponse to our InnerComponent
    const WrapperComponent = () => {
        const hookResponse = useHookWithLoadingState()
        if (hookResponse.loading) {
            return <Loading />
        }
        return <InnerComponent hookResponseData={hookResponse.data} />
    }

    return WrapperComponent
}

Our second component looks, almost the same:

const Component = withHookWithLoadingState(({hookResponseData}) => {
    // Now do the thing I wanted to do
})

But the advantage is we can re-use this without remaking our wrapper component:

const OtherComponent = withHookWithLoadingState(({hookResponseData}) => {
    // This totally works too
})

withHookWithLoadingState is a higher order component - it’s a function that takes in a component and returns a new component. In this case, the new component has hookResponseData automatically injected into it, so it doesn’t need to use the hook directly.

withAuthInfo and withRequiredAuthInfo - a.k.a. it’s not just about loading

Going back to our initial example, this was why we first made withAuthInfo and withRequiredAuthInfo.

withAuthInfo allows you to skip the loading check:

const UserWelcomeMessage = withAuthInfo(({isLoggedIn, user}) => {
    if (isLoggedIn) {
        return <div>Welcome, {user.firstName}!</div>
    } else {
        return <div>Welcome, stranger!</div>
    }
})

withRequiredAuthInfo goes one step further. In practice, we often run into cases where we know the user is logged in. Most commonly, we see this with people making dashboards where the only way anything would load at all is if the user is logged in.

It felt a little silly to litter the code with loading and isLoggedIn checks that are unnecessary. withRequiredAuthInfo skips both of them:

const UserProfilePicture = withRequiredAuthInfo(({user}) => {
    return <img src={user.pictureUrl} />
})

What happens if the user isn’t logged in? By default, we redirect them to the login page, but you can configure that.

When would I use this?

As we’ll see in the caveats section, it is a lot of work to make a HOC compared to just making a hook. Even coming up with a correct type for the HOC can be tricky.

The reason we use it at PropelAuth is because user/auth information tends to have a wide surface area. Even on pages without explicit user information, you may end up conditionally rendering parts of the page based on the user’s roles/permissions.

The main case where I’d advocate for HOCs is if you have some frequently used hooks that lead to a lot of repeated boilerplate code. If you are a library maintainer, it's worth considering if it would help reduce boilerplate in your user's products.

Higher Order Component Caveats

While I am a big fan of HOCs, it is important to mention that they do come with downsides.

Probably the biggest barrier to entry is making sure your types are correct. In our withHookWithLoadingState the returning type is a component that takes in no props. That's... not ideal.

You almost certainly want to take props in and pass them along to the child component. This means you end up with something like:

type ProvidedProps = {
    hookResponseData: HookResponseData
}

function withHookWithLoadingState<P extends ProvidedProps>(
    Component: React.ComponentType<P>
): React.ComponentType<Subtract<P, ProvidedProps>> {

    const WrapperComponent = (props: Subtract<P, ProvidedProps>) => {
        const hookResponse = useHookWithLoadingState()
        if (hookResponse.loading) {
            return <Loading />
        }

        const propsWithHookResponse: P = {
           ...props,
           hookResponseData: hookResponse.data
        }
        return <InnerComponent {...propsWithHookResponse} />
    }

    return WrapperComponent
}

Note that the inner component is type P, which is any type that has hookResponseData.

The wrapper component has the type Subtract<P, ProvidedProps> meaning that its props are every field in P except hookResponseData.

Adding on a bit, two more issues to worry about are that you have to copy static methods from the inner component to the wrapper component (but there are libraries that can do that for you) and you need to make sure you are forwarding refs from parent to child as well.

So… it’s challenging to make. It’s definitely not as simple as creating a hook and going about your day. However, if you are maintaining a library where you expect the hook to be called often enough or if you just really hate loading states, HOCs can help make your code cleaner and reduce unnecessary boilerplate.