How to reduce react context hell?

Viewed 1505

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?

3 Answers

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

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.

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 />
]}/>
Related