44

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
  • 26,684
  • 35
  • 115
  • 170
  • 4
    This is what Redux solves. – coreyward Jul 24 '18 at 17:33
  • 3
    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
  • 3
    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
  • 5
    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. – aremay Apr 29 '21 at 21:59
  • I think its better to have tried React Context and come to the conclusion that you need a state manager lie Redux rather than using Redux and not really knowing why. Remember useReducer can probably be a "before 3rd-party-state-manager" step. – Andy Jun 13 '23 at 11:35

7 Answers7

72

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.

rista404
  • 7,639
  • 2
  • 18
  • 20
  • 6
    Add properties when provider need: const { components = [], children, ...rest } = props. ...... return {acc} – Ken Lee Jul 13 '20 at 01:40
  • 6
    How to pass the `value` attribute to the context providers inside `compose`? – moys Jan 15 '22 at 12:45
  • 1
    Does the composing of the providers have any performance benefits or is it mainly a readability / code cleanliness thing? – Reina Reinhart Jan 25 '23 at 22:35
5

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
  • 594
  • 6
  • 8
3

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
  • This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/late-answers/30499467) – Andrey Dec 04 '21 at 07:49
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
  • 35,807
  • 39
  • 131
  • 194
  • 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
1

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.

akseli
  • 79
  • 8
Haswin
  • 697
  • 8
  • 20
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

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

  • 2
    Welcome to StackOverflow! When you reply to a question remember to provide clear and documented answers. Add code fragments to explain what you mean and/or provide valuable links that give further clarifications on the solution you posted! – AndreaCostanzo1 Jun 14 '21 at 07:35