An Idiosyncratic Blog

馃Р Using Context with a HoC

Published on
10 minutes read

The Provider

Context API in React enables data sharing between components at different level in the component tree. Consider this example:

const App = () => {
  const userData = useUserData() // => Method to get user info from an API

  return (
    <Layout userData={userData}>
      <Main user={userData} />
    </Layout>
  )
}
const Layout = ({ children, userData }) => {
  return (
    <>
      <Header name={userData.name} avatar={userData.avatar} />
      <Sidebar title={userData.name} summary={userData.info} />
      {children}
      <Footer />
    </>
  )
}

We have an App component that is responsible for rendering a Layout and the Main content.

Layout component is responsible to render the common components like Header, Sidebar and a Footer.

If you observe, we are passing the userData from the App component to the Layout component. Now the Layout component in itself isn't interested in using the userData, but the Header and Sidebar are.

This is just one level of prop-drilling. Now imagine a scenario where a component which is deeply nested needs to show the data which is obtained at a top level. The code becomes unmanageable real fast. This is where Context API comes in handy.

Let's revisit the example with Context API.

import { createContext, useContext, useState } from 'react'

const defaultState = {
  userData: {},
}
const UserContext = createContext(defaultState)

export const UserProvider = ({ children }) => {
  const userData = useUserData() // => Method to get user info from an API

  const contextValues = {
    // Memoize values in an ideal case to avoid re-rendering.
    userData,
  }

  return <UserContext.Provider value={contextValues}>{children}</UserContext.Provider>
}

export const useUserContext = () => {
  const context = useContext(UserContext)
  if (context === undefined || context === null) {
    throw new Error(`useUserContext must be called within UserProvider`)
  }
  return context
}

This is a usual pattern I follow when creating Contexts. It makes using of Context simpler with just a Hook.

Now the App component wraps all the children with UserProvider:

const App = () => {
  return (
    <UserProvider>
      <Layout>
        <Main />
      </Layout>
    </UserProvider>
  )
}

And the Layout component now becomes:

const Layout = ({ children }) => {
  const { userData } = useUserContext()

  return (
    <>
      <Header name={userData.name} avatar={userData.avatar} />
      <Sidebar title={userData.name} summary={userData.info} />
      {children}
      <Footer />
    </>
  )
}

No need to provide the userData via props anymore. Any component that needs the userData can just consume the useUserContext() hook and get the data, provided that component is a child under UserProvider.

Dealing with Multiple Contexts

In a real world scenario, the use cases won't be as simple as the example above. Usually there'd be multiple providers and default values for providers like:

const App = () => {
  const dark = isDarkMode()
  const lang = getPrefferedLang()

  return (
    <ThemeProvider darkMode={dark}>
      <I8nProvider lang={lang}>
        <UserProvider>
          <Layout>
            <Main />
          </Layout>
        </UserProvider>
      </I8nProvider>
    </ThemeProvider>
  )
}

In the above example, we saw that the App component itself doesn't need the value from the Contexts, but wraps its children in all the providers. If the App component wanted to access data from the context, we'd have to create another top level component and wrap App in the top level container.

I feel this becomes unmanageable very fast with extra components whose sole purpose is to wrap the children in a context provider. In order to solve this issue, let's leverage the Higher-Order-Component (Hoc) pattern.

First we create a HoC that will take an array of Providers and return a component wrapped with the same providers

const hasProperty = (object, key) => (object ? Object.hasOwnProperty.call(object, key) : false)
const hasProps = (arg) => hasProperty(arg, 'provider') && hasProperty(arg, 'props')

export const withContextProviders = (...providers) => (Component) => (props) =>
  providers.reduceRight((acc, prov) => {
    let Provider = prov
    if (hasProps(prov)) {
      Provider = prov.context
      const providerProps = prov.props
      return <Provider {...providerProps}>{acc}</Provider>
    }
    return <Provider>{acc}</Provider>
  }, <Component {...props} />)

The code looks cryptic, but is actually pretty straightforward. withContextProviders expects an array of context providers as the first argument and then the component itself. Using a reducer function, we create a providers tree, wrapping each provider with the previous provider, starting with the component.

There are multiple ways to use this HoC:

  1. Passing the providers directly to the withContextProviders HoC.
const App = () => {
  return (
    <Layout>
      <Main />
    </Layout>
  )
}

export default (ThemeProvider, I8nProvider, UserProvider)(App)
  1. Using objects, passing additional props to individual providers, specially useful when initializing a context with some default value.
const App = () => {
  return (
    <Layout>
      <Main />
    </Layout>
  )
}

export default ({ provider: ThemeProvider, props: { darkMode: true } },
{ provider: I8nProvider, props: { lang: 'en' } })(App)
  1. Same as #2, but we maintain a providers array and pass that as an argument to withContextProvider which improves code readability.
const providers = [
  { provider: ThemeProvider, props: { darkMode: true } },
  { provider: I8nProvider, props: { lang: 'en' } },
]

const App = () => {
  return (
    <Layout>
      <Main />
    </Layout>
  )
}

export default withContextProviders(...providers)(App)

Using the HoC, our App component became much cleaner. And the App component itself can now consume values from the context.

Conclusion

Contexts are a great way to share data and manage states in a React application. In this article, We revisited the Context API is, and it's usage using a Hook design pattern. We also explored how an application can have multiple providers, and lastly how we can clean up a bit using a Higher Order Component.