React: Suggestion: useProvider for replacing context.Provider

Created on 5 Jan 2019  路  7Comments  路  Source: facebook/react

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

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 a useProvider call inside, invisibly affecting everything below.

All 7 comments

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

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.

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.

Was this page helpful?
0 / 5 - 0 ratings