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

How will react solve nested contexts? #14620

Closed
rabbitooops opened this issue Jan 18, 2019 · 31 comments
Closed

How will react solve nested contexts? #14620

rabbitooops opened this issue Jan 18, 2019 · 31 comments

Comments

@rabbitooops
Copy link

rabbitooops commented Jan 18, 2019

<context1.Provider value={value1}>
  <context2.Provider value={value2}>
    <context3.Provider value={value3}>
      <context4.Provider value={value4}>
        <context5.Provider value={value5}>

        </context5.Provider>
      </context4.Provider>
    </context3.Provider>
  </context2.Provider>
</context1.Provider>
<context1.Consumer>
  {value1 => <context2.Consumer>
    {value2 => <context3.Consumer>
      {value3 => <context4.Consumer>
        {value4 => <context5.Consumer>
          {value5 => (
            null
          )}
        </context5.Consumer>}
      </context4.Consumer>}
    </context3.Consumer>}
  </context2.Consumer>}
</context1.Consumer>
@penspinner
Copy link

penspinner commented Jan 18, 2019

The upcoming Hooks API provide a different way to consume contexts.

https://reactjs.org/docs/hooks-reference.html#usecontext

@milesj
Copy link
Contributor

milesj commented Jan 18, 2019

I'll be honest. If you're encountering this kind of implementation, then your architecture design seems poor and you probably shouldn't be using React context.

@milesj
Copy link
Contributor

milesj commented Jan 18, 2019

What does that library have to do with 5 layers deep of providers/consumers?

@milesj
Copy link
Contributor

milesj commented Jan 18, 2019

In that case, the context handling is now on the users of the library, and less on the library. How they utilize the features is up to them, and if they want all providers in one place (which defeats the purpose of having multiple stores), then that's their choice. Ideally a multiple store solution would be implemented at different splits in the application, so nested contexts like this are much more rare.

My 2 cents at least.

@gaearon
Copy link
Member

gaearon commented Jan 18, 2019

Not at all. But it's also hard to discuss your problem without a more realistic example. Please create one?

@gaearon
Copy link
Member

gaearon commented Jan 18, 2019

What would your ideal syntax be?

@gaearon
Copy link
Member

gaearon commented Jan 18, 2019

Consuming multiple contexts without nesting is already supported. (With Hooks.)

Context.write has an RFC open for it. We don't know if it'll get through because it raises some very complicated questions. But while the RFC is open I'm not sure what's actionable in this issue. Do you have something to add beyond what's already in the RFC motivation?

@TrySound
Copy link
Contributor

TrySound commented Jan 18, 2019

@rabbitooops Which exactly issues with hooks do you have? I use hooks in production and they work well for my team.

@TrySound
Copy link
Contributor

TrySound commented Jan 18, 2019

@zhujinxuan
Copy link

zhujinxuan commented Jan 18, 2019

@rabbitooops How about using a single store and Symbol as keys to mimic multi-layers of store?

data Store = Leaf Object | C Store Store

Or in a imperfect way in javascript:

const LEFT = Symbol('LEFT')
const RIGHT = Symbol('RIGHT')
function createLeafStore = return new Store({});
function createStore(leftChild :: Store, rightChild :: Store) {
  return new Store({[LEFT]: leftChild, [Right]: rightChild})
}

@YosbelSardinas-minted
Copy link

YosbelSardinas-minted commented Jan 18, 2019

@zhujinxuan You can use Unstated, example:

<Subscribe to={[AppContainer, CounterContainer, ...]}>
  {(app, counter, ...) => (
    <Child />
  )}
</Subscribe>

@gaearon
Copy link
Member

gaearon commented Jan 18, 2019

Seems hooks API has lots of issues to be solved and is very unstable right now

We're preparing it for a release within a week or two — not sure why you inferred that. They'll be ready soon although if you want to be safe, please wait until a stable release.

And what about this?

Calling Hooks in a loop (as you do in forEach) is generally not allowed. It's easy to cause issues this way.

useStoreProviders

Both useProvider and useShouldComponentUpdate are problematic as Hooks (which is why React doesn't have them). See my response in #14534 (comment).


Overall, I'm struggling to understand the intent of this issue.

Consuming multiple contexts is solved by useContext Hook. We don't recommend to somehow "automate" it with arrays because this makes it too easy to write components that subscribe to too much context and re-render too often. You should have a clear sense of which contexts a component listens to, which is what useContext API gives you. If you must, you can write useMyContexts() Hook that explicitly uses specific contexts. I just don't recommend making it dynamic like you did because if the array length changes, it can break.

Putting multiple providers can be seen as "boilerplate" and we might eventually have a solution for this. But I also don't understand why you see it as a big problem. Examples in this thread aren't realistic enough to explain the issue to me. I don't see anything bad with nesting several layers of JSX somewhere at the top of the application. Pretty sure you have much deeper div nesting in most components and that doesn't hurt too much.

I'll close this as I think I already replied to these points, and the discussion goes in circles. If there's something missing let me know.

@gaearon gaearon closed this as completed Jan 18, 2019
@YosbelSardinas-minted
Copy link

YosbelSardinas-minted commented Jan 18, 2019

OT: @gaearon, there is any plan to add something like useRender or something to have more control of rendering? eg:

useRender(() => <div />, [...props])

The second arg has the same role of useEffect hook.

@gaearon
Copy link
Member

gaearon commented Jan 18, 2019

useMemo is your friend.

See second snippet in https://reactjs.org/docs/hooks-faq.html#how-to-memoize-calculations.

@0xorial
Copy link

0xorial commented May 10, 2019

I ended up with a code like that:

function provider<T>(theProvider: React.Provider<T>, value: T) {
   return {
      provider: theProvider,
      value
   };
}

function MultiProvider(props: {providers: Array<{provider: any; value: any}>; children: React.ReactElement}) {
   let previous = props.children;
   for (let i = props.providers.length - 1; i >= 0; i--) {
      previous = React.createElement(props.providers[i].provider, {value: props.providers[i].value}, previous);
   }
   return previous;
}

Then in my top-level providing component:

public render() {
      return (
         <MultiProvider
            providers={[
               provider(Context1.Provider, this.context1),
               provider(Context2.Provider, this.context2),
               provider(Context3.Provider, this.context3),
               provider(Context4.Provider, this.context4),
               provider(Context5.Provider, this.context5),
            ]}
         ><AppComponents />
      </MultiProvider>
}

@gaearon

I don't see anything bad with nesting several layers of JSX somewhere at the top of the application.

I have ~15 dependencies that I want to be injectable in that manner, and having 15 levels of indentation doesn't look pretty to me :)

@alesmenzelsocialbakers
Copy link

alesmenzelsocialbakers commented May 10, 2019

@0xorial you dont really need to have a component for that, after all <></> is just a function call React.createElement. So you could simplify it to a compose function like:

const compose = (contexts, children) =>
  contexts.reduce((acc, [Context, value]) => {
    return <Context.Provider value={value}>{acc}</Context.Provider>;
  }, children);

and use it as:

import Context1 from './context1';
import Context2 from './context2';
import Context3 from './context3';
...
import Context15 from './context15';

const MyComponent = (props) => {
  // const value1..15 = ... get the values from somewhere ;

  return compose(
    [
      [Context1, value1],
      [Context2, value2],
      [Context3, value3],
      ...
      [Context15, value15],
    ],
    <SomeSubComponent/>
  );
}

@disjukr
Copy link

disjukr commented Jul 15, 2019

I wrote a library in the past that handles this case: https://github.com/disjukr/join-react-context

@Ilham132
Copy link

Ilham132 commented Sep 17, 2019

react11

@jsdevel
Copy link

jsdevel commented Jan 21, 2020

this is definitely something that happens in applications all the time. useContext is great for consuming the contextual data in a component, but it isn't so great when you need to provide context in an app with multiple providers.

@kornfleks
Copy link

kornfleks commented Jan 29, 2020

Here is a closure alternative of @alesmenzelsocialbakers solution:

const composeProviders = (...Providers) => (Child) => (props) => (
  Providers.reduce((acc, Provider) => (
    <Provider>
      {acc}
    </Provider>
  ), <Child {...props} />)
)

const WrappedApp = composeProviders(
  ProgressProvider,
  IntentsProvider,
  EntitiesProvider,
  MessagesProvider
)(App)

ReactDOM.render(<WrappedApp />, document.getElementById('root'));

Downside is that you have to write each specific Provider component.
Example:

export const ProgressProvider = ({ children }) => {
  const [progress, setProgress] = useState(0)

  return (
    <ProgressContext.Provider value={{ progress, setProgress }}>
      {children}
    </ProgressContext.Provider>
  )
}

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

@kolodny
Copy link
Contributor

kolodny commented Apr 29, 2020

I took a crack at this as well. This seems to work fine:

const composeWrappers = (
  wrappers: React.FunctionComponent[]
): React.FunctionComponent => {
  return wrappers.reduce((Acc, Current): React.FunctionComponent => {
    return props => <Current><Acc {...props} /></Current>
  });
}

Usage is:

const SuperProvider = composeWrappers([
    props => <IntlProvider locale={locale} messages={messages} children={props.children} />,
    props => <ApolloProvider client={client}>{props.children}</ApolloProvider>,
    props => <FooContext.Provider value={foo}>{props.children}</FooContext.Provider>,
    props => <BarContext.Provider value={bar}>{props.children}</BarContext.Provider>,
    props => <BazContext.Provider value={baz}>{props.children}</BazContext.Provider>,
  ]);
  return (
    <SuperProvider>
      <MainComponent />
    </SuperProvider>
  );

I also published this helper as an npm library react-compose-wrappers

@keithkelly31
Copy link

keithkelly31 commented Jun 18, 2020

The following shows how I am passing around the authenticated user to components that need it.

I decided to create one state for my application. In my State.js file I set up my initial state, context, reducer, provider, and hook.

import React, { createContext, useContext, useReducer } from 'react';

const INITIAL_STATE = {}

const Context = createContext();

const reducer = (state, action) => 
  action 
    ? ({ ...state, [action.type]: action[action.type] }) 
    : state;

export const Provider = ({ children }) => (
  <Context.Provider value={ useReducer(reducer, INITIAL_STATE) }>
    { children }
  </Context.Provider>
);

const State = () => useContext(Context);

export default State;

Then in my index.js file I wrapped my app in the provider.

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from './State';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <Provider>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root'),
);

To consume the state in a component I can use the hook. I can also use dispatch to update the state. For example if I want to get or set a user.

import React, {useEffect} from 'react';
import State from './State'

const ExampleComponent = () => {
  const [{ user }, dispatch] = State(); 

  useEffect(() => {
    const getUser = async () => {
      const data = await fetch('http://example.com/user.json');  // However you get your data
      dispatch({ type: 'user', user: data });
    }
    getUser();
  }, [dispatch]);

  // Don't render anything until user is retrieved
  // The user is undefined since I passed an empty object as my initial state
  if(user === undefined) return null; 

  return(
    <p>{user.name}</p>
  );
}

export default ExampleComponent;

I think this way gives me the freedom to build the state how I need it without adding a ton of extra contexts and helps me to avoid a deep nest of providers.

@Emyboy
Copy link

Emyboy commented Jun 23, 2020

The upcoming Hooks API provide a different way to consume contexts.

https://reactjs.org/docs/hooks-reference.html#usecontext

How to do I use this in a class component ?

@Zhigamovsky
Copy link

Zhigamovsky commented Jun 23, 2020

The upcoming Hooks API provide a different way to consume contexts.
https://reactjs.org/docs/hooks-reference.html#usecontext

How to do I use this in a class component ?

Aren't hooks used to take advantage of various React features without writing classes?
Well, that is, everything that various hooks do is already exist in the classes. If you're talking about convenient syntax and usage api, then the react moves away from classes to functional components, so welcome to functions and hooks)

@TotooriaHyperion
Copy link

TotooriaHyperion commented Oct 16, 2020

I created a package to solve the problem by provide similar API with vue3

https://github.com/TotooriaHyperion/react-multi-provide

  • solve the problem that react context takes extra view tree.
  • also preserve reactivity of what was injected
  • fractal between providers
  • use WeakMap to store the dependencies
  • use Object wrapped symbol to achieve better typescript support, dependency injection & debug experience

notice:

  • I don't recommend you to heavily rely on context's reactivity, because it's better to provider the access to the data rather than the data itself. What @gaearon has said about don't subscribe to too many contexts was right. But he didn't mention that it's a wrong pattern to solve the data subscription by relying on context's reactivity. So the right thing to do is not to use multiple contexts but provide your dependencies in one context. And meanwhile, keep your contexts as stable as possible.
  • And thus, if we want a better API, we need handle it ourself, and keep in mind to make the API fractal. That's what my package is for.

Outer.tsx

import React, { useMemo } from "react";
import { Providers, useCreateContexts, useProvide } from "../..";
import { createService, ServiceA } from "./service";

export const Outer: React.FC = ({ children }) => {
  const contexts = useCreateContexts();
  const service = useMemo(createService, []);
  useProvide(contexts, ServiceA.id, service);
  return <Providers contexts={contexts}>{children}</Providers>;
};

Inner2.tsx

import React from "react";
import { useContexts, useReplaySubject } from "../..";
import { ServiceA } from "./service";

export const Inner2: React.FC = () => {
  const [
    {
      state$,
      actions: { inc, dec },
    },
  ] = useContexts([ServiceA.id]);
  const count = useReplaySubject(state$);
  return (
    <>
      <p>{count}</p>
      <div>
        <button onClick={inc}>Increment</button>
        <button onClick={dec}>Decrement</button>
      </div>
    </>
  );
};

@buhichan
Copy link

buhichan commented Oct 28, 2020

here is how I do it:

interface Composable {
    (node: React.ReactNode): React.ReactElement
}

const composable1: Composable = (node)=>{
      return <someContext.Provider>{node}</someContext.Provider>
}

function Comp({children}:{children?:React.ReactNode}){
       return pipe(
             composabl1, composabl2, composable3
       )(children)
}

You can find the pipe function in many popular libraries such as rxjs, there's also several language-level proposals for this pipeline-like operation. There's no need to 'solve' it by using another lib.

@waleedshkt
Copy link

waleedshkt commented Dec 4, 2020

From all the above proposed solutions to wrapping components with multiple providers, this one by @alesmenzelsocialbakers is the most performant and neat one

@jdarshan5
Copy link

jdarshan5 commented Jun 2, 2021

well I have a problem with context, this is my code
<AContext.Provider value={this.alpha}> <BContext.Provider value={this.beta}> {...children} </BContext.Provider> </AContext.Provider>
Problem is that if I try to access the context value of the AContext then it gives me the values of the BContext and I have access them in my class component.
consumption of AContext in child component is as below,
class P2 extends React.Component { static contextType = AContext; componentDidMount = () => { console.log(this.context); } render() { return( <View> <Text> 2 </Text> </View> ) } }
Here the problem is that on component did mount the console is showing the data of this.beta which is passed to the BContext but I need to access the value this.alpha which is of the AContext.
Plz help me

@nanxiaobei
Copy link

nanxiaobei commented Jun 7, 2021

react-easy-contexts is made to solve this.

👉 https://github.com/nanxiaobei/react-easy-contexts

An introduction article:

https://nanxiaobei.medium.com/react-easy-contexts-add-multiple-react-contexts-in-the-simplest-way-915d15cca5ca

@pandanoir
Copy link

pandanoir commented Dec 25, 2021

I published package that provides type-safe way to merge providers flat.

https://www.npmjs.com/package/react-merge-providers

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

No branches or pull requests