馃Р Using Context with a HoC
- Published on
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
}
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:
- Passing the providers directly to the
withContextProviders
HoC.
const App = () => {
return (
<Layout>
<Main />
</Layout>
)
}
export default (ThemeProvider, I8nProvider, UserProvider)(App)
- 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)
- Same as #2, but we maintain a
providers
array and pass that as an argument towithContextProvider
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.
This is a usual pattern I follow when creating Contexts. It makes using of Context simpler with just a Hook.