DEV Community

Maxim
Maxim

Posted on • Originally published at maximzubarev.com on

Wrap providers elegantly using withProvider HoC

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.)

Top comments (0)