Reputation: 27972
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
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
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
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
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
Reputation: 1
recompose js nest helper if you need inject external props to provider elemet use withprops hoc
Upvotes: 0
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
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
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