How to reduce react context hell?
Asked Answered
C

5

19

I have inherited a codebase where the previous owner has made extensive use of React.Context. This has resulted in what might be described as "context hell"

<AppContextProvider>
  <AnotherProvider>
    <AgainAnotherProvider configProp={false}>
      <TestProvider>
        <FooProvider>
          <BarProvider configHereAlso={someEnvronmentVar}>
            <BazProvider>
              <BatProvider>
                <App />
              </BatProvider>
            </BazProvider>
          </BarProvider>
        </FooProvider>
      </TestProvider>
    </AgainAnotherProvider>
  </AnotherProvider>
</AppContextProvider>;

This feels like an anti-pattern and is causing considerable cognitive overhead in understanding how the whole application works.

How do I fix this? Is it possible to use just one provider for everything? I previously used redux-toolkit with redux for managing state in react. Is there anything similar for contexts?

Crin answered 10/5, 2021 at 9:25 Comment(6)
You could use one provider for everything and make that really complicated, but I'm guessing that was what the original coder was trying to prevent using a number of bespoke contexts to deliver state to the app. I'd advise you to take some time to learn what's going on here before you make any drastic changes.Awful
Is your objection purely to the syntactically deep indentation? Using contexts like this does not impede performance of the application, in fact quite the opposite. Since the <App> component itself should be pure, this tree will never re-render, and only dispatches from within the application will ever re-render specific context providers (and all their consumers, as it should), skipping re-rendering of the context providers which are nested within.Artillery
Perhaps a compromise: How to eliminate deeply-nested React context hell? Although... I mean, the canonical solution is pretty straight-forward: combine contexts.Maryn
@DaveNewton disagree with your suggested canonical solution. Combining contexts will degrade performance. Specialized changes to provided values will re-render more consumers than necessary. Keeping contexts separate has a very clear benefit in that regard. Title suggestion is fine though.Artillery
@PatrickRoberts I didn't say it was a good solution in all cases, I said "the way to eliminate deeply-nested contexts is to combine contexts". Whether or not it's "good" is context-dependent.Maryn
This as it stands is an opinionated set of answers. Like why I might like red better than the blue or green someone else might like on their hat. Given you have not accepted another's answer and provide none of your own perhaps you might restate the question by editing it to be more definitive?Mobility
J
7

I found an elegant solution:

const Providers = ({providers, children}) => {
  const renderProvider = (providers, children) => {
    const [provider, ...restProviders] = providers;
    
    if (provider) {
      return React.cloneElement(
        provider,
        null,
        renderProvider(restProviders, children)
      )
    }

    return children;
  }

  return renderProvider(providers, children)
}

ReactDOM.render(
  <Providers providers={[
    <FooContext.Provider value="foo" />,
    <BarContext.Provider value="bar" />,
    <BazContext.Provider value="baz" />,
  ]}>
    <App />
  </Providers>,
  document.getElementById('root')
);
Janycejanyte answered 28/8, 2021 at 8:4 Comment(3)
Please describe IN your answer why you believe it IS an elegant solution but also "what" the solution represents in comparison to what the OP hasMobility
In this solutions there's a problem with components that require children, e.g. in TS this sometimes can't pass.Respond
Not a recommended approach by React react.dev/reference/react/cloneElement#alternativesPeriostitis
E
2

One way to get rid of additional contexts is by adding more variables to the value that is provided by a context.

For example if there are two contexts UserContext and ProfileContext with providers like:

<UserContext.Provider value={user}... and

<ProfileContext.Provider value={profile}...

then you can merge them into a single context:

<UserProfileContext.Provider value={{user, profile}}...

Note: This doesn't mean you should merge all contexts into a single context because of the separation of concerns and all consumers rerender when a context's value changes leading to unwanted renders.

Editorial answered 10/5, 2021 at 9:31 Comment(2)
Also, remember that contexts should also only be used if the value has to be used in many places and/or deep within the tree. Otherwise, just pass values as properties.Balladeer
Please don't do this without being aware of the consequences on your app's performance. I suggest you read this article which sums it up nicely: thoughtspile.github.io/2021/10/04/react-context-dangersLisabethlisan
S
1

You could use this npm package react-pipeline-component

Your code would be like this:

import {Pipeline, Pipe} from 'react-pipeline-component'

<Pipeline components={[
    <AppContextProvider children={<Pipe />} />,
    <AnotherProvider children={<Pipe />} />,
    <AgainAnotherProvider configProp={false} children={<Pipe />} />,
    <TestProvider children={<Pipe />} />,
    <FooProvider children={<Pipe />} />,
    <BarProvider configHereAlso={someEnvronmentVar} children={<Pipe />} />,
    <BazProvider children={<Pipe />} />,
    <BatProvider children={<Pipe />} />,
    <App />
]}/>
Stokes answered 25/5, 2022 at 8:33 Comment(0)
P
0

Just published a package for this:

https://www.npmjs.com/package/compose-children

It turns this:

type ComposedComponentProps = Component1Props &
  Component2Props &
  Component3Props;

const ComposedComponent = ({ children, ...props }: ComposedComponentProps) => (
  <Component1 {...props}>
    <Component2 {...props}>
      <Component3 {...props}>{children}</Component3>
    </Component2>
  </Component1>
);

Into this:

import composeChildren from "compose-children";

type ComposedComponentProps = Component1Props &
  Component2Props &
  Component3Props;

const ComposedComponent = composeChildren<ComposedComponentProps>(
  Component1,
  Component2,
  Component3,
);
Plangent answered 12/11, 2023 at 21:28 Comment(0)
C
0

What is your biggest issue here?

The way the code looks with the deep nesting at the App level (I like Pipeline and composeChildren solutions already provided) or the cognetive overhead involved with understanding what does where?

Does the whole app need access to all of these seperate contexts?

Firstly, one provider in context would lead to the whole app re-rendering everytime the value of said context provider changes. Unless the app is very small this could have some noticable effects on user experience down the track and would not be worth the tradeoff for better looking code.

If I inherited this codebase, and the cognetive overhead, I would draw a diagram of the whole tree and mark out where each of these contexts are used. This would be a good way of starting to reduce the cognetive overhead you and give you some easier to understand context (pardon the pun) for how the app works.

If I had time to refactor and if the providers can wrap components further down the chain I would start with that. No use having a context wrap components at the app level if it's only used by a couple of leaf components.

Also, the aformentioned diagram could be added to the documentation somewhere along with some general rules for the context and where/how you want to use it. As annoying as this exercise would be at first it will give you a much better understanding of how the system as a whole works and there will be plenty of oppertunities for performance optimization.

Global state management in React is a difficult topic full of potential pitfalls and the pattern displayed here could easily see the whole app re rendering everytime a controlled input is used if you're not careful. Nadia Makrevich wrote a very good article on context and it's strengths and weaknesses if you have time to check it out:

https://www.developerway.com/posts/how-to-write-performant-react-apps-with-context

Cathartic answered 12/11, 2023 at 23:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.