loading...

Wrap providers elegantly using withProvider HoC

mxmzb profile image Maxim Originally published at maximzubarev.com on ・2 min read

If you use React contexts, you probably have created a wrapper component before, which includes just the context Provider. You do that because you need the context data inside some component, but for it to be available, the Provider can't be rendered in the same component. It needs to be rendered somewhere above.

Here is a pattern that I use from time to time. It allows me to work with React contexts in a very compressed manner (I mean in the same file). Let's look at an example:

import React, { useContext, useState } from "react";

const CounterContext = React.createContext();

const CounterProvider = ({ children }) => {
  const [count, setCount] = useState(0);

  return (
    <CounterContext.Provider
      value={{
        count,
        increment: () => setCount((prevCount) => prevCount + 1),
      }}
    >
      {children}
    </CounterContext.Provider>
  );
};

const App = () => {
  const { increment, count } = useContext(CounterContext);

  return (
    <div>
      <h2>{count}</h2>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

// 👇 without this 🤡, useContext in App will return undefined.
const WrappedApp = () => (
  <CounterProvider>
    <App />
  </CounterProvider>
);

export default WrappedApp;

If we create a simple higher-order component, we can use that instead of arbitrarily creating provider wrappers like WrappedApp.

const withBasicProvider = (Provider) => (Component) => (props) => (
  <Provider>
    <Component {...props} />
  </Provider>
);

// ...

export default withBasicProvider(CounterProvider)(WrappedApp);

This is a very naive implementation and will simply wrap a component with the passed argument, so you can omit creating a provider wrapper component manually.

But what if we have more than one context, which we want to use? We can reduce any number of given providers to incrementally wrap the passed component.

I'm using reduceRight just, to preserve the order of passed providers (e.g. the CounterProvider wraps DarkModeProvider wraps App):

export const withBasicProviders = (...providers) => (WrappedComponent) => (props) =>
  providers.reduceRight((acc, Provider) => {
    return <Provider>{acc}</Provider>;
  }, <WrappedComponent {...props} />);

// somewhere here `DarkModeProvider` has been added, too.
// It really doesn't matter how it looks in detail. This
// HoC is just about handling providers. In the end there
// is a CodeSandbox link for you to play around with
// actually working code.

// ...

export default withBasicProviders(CounterProvider, DarkModeProvider)(App);

Now, to make this HoC truly useful, let's see how to pass props to their respective providers. I am sure you can implement that functionality in various ways, but I feel the following is quite neat.

If there are any props that you want a provider to receive, you can simply pass an array. The HoC will check if the current provider item is an array. If it is, withProviders will use the object, that the second item is supposed to be, as props. If it isn't, it behaves just like withBasicProviders before - it just wraps the previous element.

export const withProviders = (...providers) => (WrappedComponent) => (props) =>
  providers.reduceRight((acc, prov) => {
    let Provider = prov;
    if (Array.isArray(prov)) {
      Provider = prov[0];
      return <Provider {...prov[1]}>{acc}</Provider>;
    }

    return <Provider>{acc}</Provider>;
  }, <WrappedComponent {...props} />);

// ...

export default withProviders([CounterProvider, { start: 5 }], DarkModeProvider)(App);

If you want to play around with some working code, here's a CodeSandbox demo.

(This is an article posted to my blog at maximzubarev.com. You can read it online by clicking here.)

Discussion

pic
Editor guide