function App(props) {
const [theme] = useState({color: 'white'})
const [user] = useState({name: 'rabbit'})
useProvider(themeContext, theme)
useProvider(userContext, user)
/* ... */
}
is equivalent to
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
theme: {color: 'white'},
user: {name: 'rabbit'}
}
}
render() {
return (
<themeContext.Provider value={this.state.theme}>
<userContext.Provider value={this.state.user}>
</userContext.Provider>
</themeContext.Provider>
)
}
}
Solution for class component:
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
theme: {color: 'white'},
user: {name: 'rabbit'}
}
}
static withProviders() {
const {theme, user} = this.state
return [
[themeContext, theme],
[userContext, user]
]
}
}
It looks a little bit stupid but better than nested Providers.
Providers intentionally don't work this way because the notion of tree structure actually has a meaning (unlike Hooks like useState). Your proposal makes it more difficult to move code between components because any Hook could contain a useProvider call inside, invisibly affecting everything below.
I still think this is a valid suggestion and even considering the issue you described does not outweighs the pros in my opinion.
Even Vue added this to their draft for "hooks": https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#dependency-injection
Being able to provide context values down the tree using a hook instead of a wrapper component would open soo many more possibilities that this should definitely be evaluated again!
Edit: Using a wrapper function it is possible to provide such a feature right now, but it would require always wrapping the component with withProviders when using the useProvider hook
import React from "react";
let activeProviders: Map<React.Context<any>, any> | undefined;
export function withProviders<P = any>(renderFunc: (props: P) => React.ReactElement<any>) {
return (props: P) => {
const providers = activeProviders = new Map<React.Context<any>, any>();
let content = renderFunc(props);
activeProviders = undefined;
providers.forEach((v, P) => {
content = <P.Provider value={v} children={content} />;
});
return content;
}
}
export function useProvider<T>(ctx: React.Context<T>, value: T) {
if (!activeProviders)
throw new Error('withProviders wrapper required!');
activeProviders.set(ctx, value);
}
const TestContext1 = React.createContext(1);
const TestContext2 = React.createContext(2);
export const ChildCmp: React.FC = () => {
const value1 = React.useContext(TestContext1);
const value2 = React.useContext(TestContext2);
return (
<div>
<div>Child value 1 = {value1}</div>
<div>Child value 2 = {value2}</div>
</div>
)
};
export const ParentCmp: React.FC = withProviders(() => {
useProvider(TestContext1, 10);
useProvider(TestContext2, 20);
const value1 = React.useContext(TestContext1);
const value2 = React.useContext(TestContext2);
return (
<div>
<div>Value 1 = {value1}</div>
<div>Value 2 = {value2}</div>
<ChildCmp />
</div>
)
});
Providers intentionally don't work this way because the notion of tree structure actually has a meaning (unlike Hooks like
useState). Your proposal makes it more difficult to move code between components because any Hook could contain auseProvidercall inside, invisibly affecting everything below.
I don't understand this objection, can you speak further?
It seems like a useProvider hook would be difficult to compose, which is true, but I could say the same about useEffect. Any hook that has useEffect within it could invisibly affect any node in the DOM, which seems like a failure of compositionality akin to useProvider. In practice it works fine, because it's clear from the docs that useEffect has some dangerous properties.
I have created a state management library that is better at service composition. Here is a demo of avoiding provider hell. Feel free to try it or read its source(100 lines of code)!
It introduce a "scope" object to collect the context provider, so that:
Providers intentionally don't work this way because the notion of tree structure actually has a meaning (unlike Hooks like
useState). Your proposal makes it more difficult to move code between components because any Hook could contain auseProvidercall inside, invisibly affecting everything below.
Yes it's not a good pattern to make provider a hook, but why not attach all the Contexts to the root element of the Component? It don't break the tree structure.
Or make a multiple provider(don't use contexts.reduce inside, instead, use only one layer in the tree):
<Providers contexts={[
[Context1, value1],
[Context2, value2],
]}>
{children}
</Provide>
similar to
@Module({ Providers: [Context1, Context2]})
class Component {}
we need tree structure between context and component, but don't need that between contexts.
It doesn't make a significant difference unless one use many contexts, but just looks more make sense.
Try react-multi-provide
https://github.com/facebook/react/issues/14534#issuecomment-700510943
Most helpful comment
Providers intentionally don't work this way because the notion of tree structure actually has a meaning (unlike Hooks like
useState). Your proposal makes it more difficult to move code between components because any Hook could contain auseProvidercall inside, invisibly affecting everything below.