jonhobbs
jonhobbs

Reputation: 27972

Too many React Context providers

New to react here and trying to wrap my head round the new Context API (I haven't looked into Redux etc. yet).

Seems I can do much of what I need to do, but I'm going to end up with lots and lots of providers, all needing a tag to wrap my main app.

I'm going to have a provider for Auth, one for theming, one for chat messages (vis Pusher.com) etc. Also using React Router is another wrapper element.

Am I going to have to end up with this (and many more)....

<BrowserRouter>
    <AuthProvider>
        <ThemeProvider>
            <ChatProvider>
                <App />
            </ChatProvider>
        </ThemeProvider>
    </AuthProvider>
</BrowserRouter>

Or is there a better way?

Upvotes: 49

Views: 26341

Answers (8)

machineghost
machineghost

Reputation: 35795

The top answer is great, but it doesn't let you provide props to your context providers. For instance, let's say you have two providers, and one takes an options argument:

<AppRouterCacheProvider options={{ enableCssLayer: true }}>
  <SessionContextProvider>
    {children}
  </SessionContextProvider>
</AppRouterCacheProvider>

The top answer won't let you pass those options ... so I created a variation of it that let's you provide props:

<Compose
  components={[
    [AppRouterCacheProvider, { options: { enableCssLayer: true } }],
    [SessionContextProvider],
  ]}
>
  {children}
</Compose>

You can create that Compose component with the following code (it's Typescript, but JS devs can just remove all the weird : whatever stuff after the arguments)

/**
 * Returns the provided components (instantitated with any provided props)
 * wrapped around the provided children.
 */
const Compose = ({
  components: providers,
  children,
}: {
  components: [component: FC<{ children: ReactNode }>, props?: any][];
  children: ReactNode;
}) => (
  <>
    {providers.reduceRight(
      (children: ReactNode, [Component, props]) => (
        <Component {...props}>{children}</Component>
      ),
      children,
    )}
  </>
);

Upvotes: 0

rista404
rista404

Reputation: 7787

If you want a solution for composing Providers without any third-party libraries, here's one with Typescript annotations:

// Compose.tsx

interface Props {
    components: Array<React.JSXElementConstructor<React.PropsWithChildren<unknown>>>
    children: React.ReactNode
}

export default function Compose(props: Props) {
    const { components = [], children } = props

    return (
        <>
            {components.reduceRight((acc, Comp) => {
                return <Comp>{acc}</Comp>
            }, children)}
        </>
    )
}

Usage:

<Compose components={[BrowserRouter, AuthProvider, ThemeProvider, ChatProvider]}>
    <App />
</Compose>

You can of course remove the annotations if you don't use Typescript.

Upvotes: 87

Haswin
Haswin

Reputation: 707

One simple solution for this is to use a compose function, like the one Redux uses, to combine all the providers together. Then the compose function would be called like so:

const Providers = compose(
    AuthProvider,
    ThemeProvider,
    ChatProvider
);

also I haven't used this solution but with React's new hooks feature, instead of rendering your contexts, you can use the react hook to access it in the function definition.

Upvotes: 1

Valerio Ageno
Valerio Ageno

Reputation: 134

I haven't enough reputation to comment but it could be useful integrate the rrista404 answer wrapping the component in a useCallback() hook to ensure context data integrity in some case like page switching.

// Compose.tsx

interface Props {
    components: Array<React.JSXElementConstructor<React.PropsWithChildren<any>>>
    children: React.ReactNode
}

const Compose = useCallback((props: Props) => {
    const { components = [], children } = props

    return (
        <>
            {components.reduceRight((acc, Comp) => <Comp>{acc}</Comp>, children)}
        </>
    )
}, [])

export default Compose

Upvotes: 3

Azat Temirbek uulu
Azat Temirbek uulu

Reputation: 1

recompose js nest helper if you need inject external props to provider elemet use withprops hoc

Upvotes: 0

Milan Majer
Milan Majer

Reputation: 723

Solution with for loop:

export const provider = (provider, props = {}) => [provider, props];

export const ProviderComposer = ({providers, children}) => {
    for (let i = providers.length - 1; i >= 0; --i) {
        const [Provider, props] = providers[i];
        children = <Provider {...props}>{children}</Provider>
    }
    return children;
}

Usage:

<ProviderComposer
    providers={[
        provider(AuthProvider),
        provider(ThemeProvider),
        provider(MuiPickersUtilsProvider, {utils: DateFnsUtils}),
    ]}
>
    <App/>
</ProviderComposer>

Upvotes: 6

dance2die
dance2die

Reputation: 36955

Use @rista404's answer - https://stackoverflow.com/a/58924810/4035
as react-context-composer is deprecated.

Thanks @AO17, for the ping.


Disclaimer: I've never used this, just researched.

FormidableLabs (they contribute to many OSS projects) has a project called, react-context-composer

It seems to solve your issue.

React is proposing a new Context API. The API encourages composing. This utility component helps keep your code clean when your component will be rendering multiple Context Providers and Consumers.

Upvotes: 2

Hellon Canella Machado
Hellon Canella Machado

Reputation: 456

Few lines of code solve your problem.

import React from "react"
import _ from "lodash"

/**
 * Provided that a list of providers [P1, P2, P3, P4] is passed as props,
 * it renders
 *
 *    <P1>
        <P2>
          <P3>
            <P4>
              {children}
            </P4>
          </P3>
        </P2>
      </P1>
 *
 */

export default function ComposeProviders({ Providers, children }) {
  if (_.isEmpty(Providers)) return children

  return _.reverse(Providers)
    .reduce((acc, Provider) => {
      return <Provider>{acc}</Provider>
    }, children)
}

Upvotes: 1

Related Questions