Com a introdução do hook useContext ficou mais prático de trabalhar com estado global no React, sem a necessidade de usar vários consumers no JSX. Porém, ainda há a necessidade de declarar o Provider dentro JSX, então sem perceber podemos entrar no Provider Hell como este:

function App() {
  return (
    <ThemeProvider>
      <RouterProvider>
        <SettingsProvider>
          <UserProvider>
            <MagicProvider>
              <LayoutProvider>
                <Header />
                <Body />
                <Footer />
              </LayoutProvider>
            </MagicProvider>
          </UserProvider>
        </SettingsProvider>
      </RouterProvider>
    </ThemeProvider>
  );
}

Não chega a ser um problema grave esse "bico de pato", mas com certeza não é nada agradável de se ver.

Composição

Se o prolema for somente o design do código, podemos refatorar o código usando uma função para compor os providers:

const composeProviders =
  (...providers) =>
  (props) =>
    providers.reduceRight(
      (children, Provider) => <Provider {...props}>{children}</Provider>,
      props.children,
    );

export const AllProviders = composeProviders(
  ThemeProvider,
  RouterProvider,
  SettingsProvider,
  UserProvider,
  MagicProvider,
  LayoutProvider,
);

Agora é só substituir aquele Provider Hell pelo novo AllProviders:

function App() {
  return (
    <AllProviders>
      <Header />
      <Body />
      <Footer />
    </AllProviders>
  );
}

Se quiser passar alguma propriedade para todos os providers, você pode passá-lo como propriedade do AllProviders:

<AllProviders env="dev" baseUrl="/">
  {/* ... */}
</AllProviders>

Caso queira passar uma propriedade só para um provider, basta encapsulá-lo em uma função:

export const AllProviders = composeProviders(
  ThemeProvider,
  RouterProvider,
  SettingsProvider,
  () => <UserProvider logoutRedirectTo="/login" />,
  MagicProvider,
  LayoutProvider,
);

Estado global

É confortável sair criando vários providers e colocá-los todos na raiz como demonstrado no exemplo, pode até ser uma decisão consciente com o motivo de separar bem a responsabilidade de cada estado. Mas lembre-se que quanto mais alto na hierarquia fica o estado, maior o impacto de sua atualização.

Reveja seus providers e reavalie se realmente há necessidade de ficarem global, ou se podem pelo menos descer na hierarquia dos componentes. Se contextos compartilham a mesma lógica, veja a possibilidade de agrupá-los.

E o Redux?

Trocar para o Redux não irá resolver este problema em questão, porque o Redux trabalha da mesma forma que o React Context, então em muitos casos você só estará adicionando uma biblioteca a mais para chegar ao mesmo resultado.

Curiosidade

Quando eu estava na faculdade, lá no começo da internet, eu vi um código de um amigo que estava com esse encadeamento gigantesco, e fiz essa brincadeira:

Code Hadouken MEME

Na época nem existia o conceito de MEME, mas acabou sendo usado até hoje. =)