1

I like to use contexts in my React and React Native projects, which means multiple context providers per project. As a result, the root of my app often looks like this:

<ContextA.Provider value={valueA}>
    <ContextB.Provider value={valueB}>
        <ContextC.Provider value={valueC}>
            // ...and so on until rendering actual app content
        </ContextC.Provider>
    </ContextB.Provider>
</ContextA.Provider>

This creates a pyramid of providers that looks and feels like bad style/practice.

I could lump my context values together into one big provider:

<BigContext.Provider value={ valueA, valueB, valueC }>
   /// app content
</BigContext.Provider>

...but there's a few good reasons to want to keep contexts separate - mainly preventing components that are only interested in valueA from re-rendering when only valueB changes, for example.

Even without contexts, you can still have providers from different packages stack up into their own pyramids. Here's the root of one of my React Native apps, for example:

<DataContext.Provider value={data}>
    <AppearanceProvider>
        <SafeAreaProvider>
            <NavigationContainer>
                <Tab.Navigator>
                    // tab screens here
                </Tab.Navigator>
            </NavigationContainer>
        </SafeAreaProvider>
    </AppearanceProvider>
</DataContext.Provider>

Is there a clean way to "collapse", or somehow avert, these Pyramids of Doom?

Joel Rummel
  • 802
  • 5
  • 18
  • 1
    I personally keep my providers in a seperate component called `Providers` and only use that component to wrap my app content. I still create that pyramid but keep it away from eyes. Unfortunately there is no way to avoid this. Only option is to use a state management library like redux or mobx but it might be a overkill for small projects – Ugur Eren Jul 27 '21 at 18:39
  • 1
    The only kind of bad style/practice about this could be that you're lumping together providers of different types. For example let's say that you have multiple providers for your state management and multiple ones for navigation. What you could do is create a component called `StateManagementProviders` and put all of the relevant providers there, and another one called `NavigationProviders`. Then you could just nest those two providers in your `App` component. – Kapobajza Jul 27 '21 at 18:44
  • 1
    In some sense, you can say the having multiple nested divs are Divs of Doom, don't see a valuable reason to try collapsing it – Dennis Vash Jul 27 '21 at 18:46
  • @DennisVash You're not wrong, but in this case none of the nested providers have any more than one child, creating a straight pyramid. If you've got 5+ nested divs with only one child each, you're usually doing something wrong. – Joel Rummel Jul 27 '21 at 19:01

2 Answers2

1

There isn't any performance benefit by "cleaning up" the so called Pyramid of Doom. Having multiple levels is completely fine.

Before you implement the code below, maybe make sure you don't simply provide contexts at the global level just because you can. A context provider must be wrapped close to the component that consumes it, this means not always at the root level.

That said, here is a simple Higher order component that can wrap multiple Providers to provide a Redux compose esque API.

/**
 * Check if the object has a property matching the key
 * NOTE: Works only for shallow objects.
 * @param object
 * @param key
 * @returns {boolean|boolean}
 */
const hasProperty = (object, key) =>
  object ? Object.hasOwnProperty.call(object, key) : false;
const hasProps = (arg) =>
  hasProperty(arg, 'provider') && hasProperty(arg, 'props');

export const withContextProviders = (...providers) => (Component) => (props) =>
  providers.reduceRight((acc, prov) => {
    let Provider = prov;
    if (hasProps(prov)) {
      Provider = prov.context;
      const providerProps = prov.props;
      return <Provider {...providerProps}>{acc}</Provider>;
    }
    return <Provider>{acc}</Provider>;
  }, <Component {...props} />);

Usage:

const Component = () => {...}
export withContextProviders(ThemeProvider, I8nProvider)(Component)
// OR
export withContextProviders({provider: ThemeProvider, props: {darkMode: true}}, I8nProvider)(Component)
// OR
export withContextProviders(ThemeProvider, {provider: I8nProvider, props: {lang: 'en'}})(Component)
// OR
const providers = [{provider: ThemeProvider, props: {darkMode: true}}, {provider: I8nProvider, props: {lang: 'en'}}]
export withContextProviders(...providers)(Component)

NOTE: This does not make singletons, i.e if two components are wrapped with this HoC, both of them will get their own instance of context. For cases like this, it's recommended to wrap the component with provider at the root level

Again, having multiple levels is completely fine.

PsyGik
  • 3,535
  • 1
  • 27
  • 44
  • Thanks for the detailed answer. I recognize that there's no real performance benefit to "cleaning up" the pyramid, as this was mostly a question of style. But that code looks to be a decent alternative to having a literal pyramid of code. – Joel Rummel Jul 27 '21 at 19:08
0

The "pyramid of doom" can be collapsed like this:

import { cloneElement } from "react";

function MyProviders({children}) {
  return [
    <ProviderOne />
    <ProviderTwo />
    <ProviderThree />
    <ProviderFour />
    <ProviderFive>{children}</ProviderFive>
  ].reduceRight((accumulator, currentValue) => cloneElement(currentValue, {}, accumulator))
}