Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: useProvider for replacing context.Provider #14534

Closed
Coooooooola opened this issue Jan 5, 2019 · 7 comments
Closed

Suggestion: useProvider for replacing context.Provider #14534

Coooooooola opened this issue Jan 5, 2019 · 7 comments

Comments

@Coooooooola
Copy link

Coooooooola commented Jan 5, 2019

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>
    )
  }
}
@Coooooooola Coooooooola changed the title Suggestion: A new desgin of context.Provider Suggestion: useProvider for replacing context.Provider Jan 5, 2019
@Coooooooola
Copy link
Author

Coooooooola commented Jan 5, 2019

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.

@gaearon
Copy link
Collaborator

gaearon commented Jan 18, 2019

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.

@olee
Copy link

olee commented Jun 14, 2019

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>
    )
});

@nhusher
Copy link

nhusher commented Feb 25, 2020

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

@csr632
Copy link

csr632 commented Mar 16, 2020

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:

  • Services can be isolated or composed, depending on whether they are in same scope.
    • Services can consume former services in same scope, despite that they are in the same component.
  • All the providers collected by a scope can be provided at onece, avoiding provider hell.
  • The "parent scope & child scope" structure will form a "tree" to resolve requests properly.

@TotooriaHyperion
Copy link

TotooriaHyperion commented Sep 29, 2020

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.

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.

@TotooriaHyperion
Copy link

Try react-multi-provide

#14534 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants