30

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?

jonhobbs
  • 21,469
  • 23
  • 94
  • 144
  • 2
    This is what Redux solves. – coreyward Jul 24 '18 at 17:33
  • 2
    Hmm, I was afraid somebody might say that, but I'm trying to heed the advice of those that have said to try to learn state in React before resorting to Redux. Having had a little look at Redux and MoX I think I'll be more likely to try MobX – jonhobbs Jul 24 '18 at 18:36
  • 2
    The above is a good use case for Redux; the push back is because local state is often fine. You don't want to accept unnecessary tradeoffs. See [this excellent writeup by Redux-author Dan Abramov, “You Might Not Need Redux”](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367). – coreyward Jul 24 '18 at 18:42
  • A ear a lots of people say that API Context or React hooks will put Redux to the trash but Redux is still Redux and all 3 methods should be used for different systems. In th e case or you have global store you need to affect all you website: Redux is the key, and will be ever more powerfull than API Context (by avoiding Component to ride up all the DOM (for auth or chat provider for example). Theme can be updated in Cascading like CSS so API context is a better choice. – Arthur Feb 20 '20 at 14:24
  • 2
    Does this pattern actually create any problems other than the fact that the list is visually long which makes the viewable page wide as well? – shawn98ag May 08 '20 at 01:52
  • I like to also consider whether my entire App needs access to all the information from every context. Browers or Themes or Auths etc obviously would, but I sometimes find that some of this can be abated by putting contexts as far down as possible in the subtree. – reustin Apr 29 '21 at 21:59

5 Answers5

31

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

rista404
  • 6,639
  • 2
  • 15
  • 19
  • 3
    Add properties when provider need: const { components = [], children, ...rest } = props. ...... return {acc} – Ken Lee Jul 13 '20 at 01:40
2

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.

dance2die
  • 31,758
  • 34
  • 122
  • 177
  • That looks very promising. I don't entirely understand their example code (e.g. WTH is Context?), but I'm going to give it a go and will mark as correct if it works. Many thanks. – jonhobbs Jul 24 '18 at 23:49
  • 1
    @jonhobbs The example uses `flow` according to this example, https://github.com/jamiebuilds/create-react-context#example – dance2die Jul 25 '18 at 00:15
  • 1
    the package has been archived, use the solution from @rista404 – AO19 Feb 20 '20 at 08:19
  • @AO17 Much appreciated for heads-up~ Updated the answer. – dance2die Feb 20 '20 at 14:19
2

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>
Milan Majer
  • 103
  • 1
  • 3
1

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)
}
0

One simple solution for this is to use something similar to compose function in https://github.com/reduxjs/redux/blob/master/src/compose.js and combine all the providers together.

provider.js

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.

Haswin
  • 527
  • 4
  • 19