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