13

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?

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
spinners
  • 2,449
  • 3
  • 23
  • 34
  • 3
    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. – Andy May 10 '21 at 09:35
  • 3
    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 `` 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. – Patrick Roberts May 10 '21 at 14:33
  • 1
    Perhaps a compromise: How to eliminate deeply-nested React context hell? Although... I mean, the canonical solution is pretty straight-forward: combine contexts. – Dave Newton May 10 '21 at 15:17
  • 1
    @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. – Patrick Roberts May 10 '21 at 15:20
  • @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. – Dave Newton May 10 '21 at 15:21
  • 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? – Mark Schultheiss Feb 03 '22 at 21:34

3 Answers3

3

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')
);
1

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.

Ramesh Reddy
  • 10,159
  • 3
  • 17
  • 32
  • 5
    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. – Peter Lehnhardt May 10 '21 at 09:34
  • 1
    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: https://thoughtspile.github.io/2021/10/04/react-context-dangers/ – Mickaël Gagné Feb 03 '22 at 19:52
0

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 />
]}/>
LSafer
  • 344
  • 3
  • 7