React: Como vai reagir resolver contextos aninhados?

Criado em 18 jan. 2019  ·  38Comentários  ·  Fonte: facebook/react

Eu tenho muitos contextos e tenho que escrever assim, tão feio! Isso atrapalha meu trabalho agora. Esse design o torna quase inutilizável.

<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>
Question

Comentários muito úteis

@ 0xorial você realmente não precisa ter um componente para isso, afinal <>> é apenas uma chamada de função React.createElement. Então, você pode simplificar para uma função de composição como:

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

e usá-lo como:

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/>
  );
}

Todos 38 comentários

A próxima API Hooks fornece uma maneira diferente de consumir contextos.

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

Obrigado. Mas e quanto ao provedor?

Eu serei honesto. Se você está encontrando esse tipo de implementação, então o design de sua arquitetura parece ruim e você provavelmente não deveria estar usando o contexto React.

Não, estou projetando um novo contêiner de loja. Ele precisa trabalhar com o contexto em reação.
https://github.com/rabbitooops/rako

O que essa biblioteca tem a ver com 5 camadas de fornecedores / consumidores?

Porque ele suporta a injeção de várias lojas para reagir em vez de uma solução de loja única como o Redux.

Nesse caso, o tratamento de contexto agora está nos usuários da biblioteca, e menos na biblioteca. Como eles utilizam os recursos é com eles, e se eles querem todos os provedores em um só lugar (o que anula o propósito de ter várias lojas), a escolha é deles. Idealmente, uma solução de várias lojas seria implementada em diferentes divisões no aplicativo, portanto, contextos aninhados como esse são muito mais raros.

Meus 2 centavos, pelo menos.

Portanto, a API de contexto é amigável apenas para soluções de loja única como redux.

De modo nenhum. Mas também é difícil discutir seu problema sem um exemplo mais realista. Por favor, crie um?

Imagine que existem três lojas temáticas, usuário e contador.

function theme(getState) {
  return {
    color: 'white',
    setColor(color) {
      this.setState({color})
    }
  }
}

function user(getState) {
  return {
    name: '',
    setName(name) {
      this.setState({name})
    }
  }
}

function counter(getState) {
  return {
    value: 0,
    increment() {
      const {value} = getState()
      this.setState({value: value + 1})
    }
  }
}

const [themeStore, userStore, counterStore] = createStores(theme, user, counter)
const [themeContext, userContext, counterContext] = createContexts(themeStore, userStore, counterStore)


class App extends React.Component {
  render() {
    return (
      <themeContext.StoreProvider>
        <userContext.StoreProvider>
          <counterContext.StoreProvider>

            <Child />

          </counterContext.StoreProvider>
        </userContext.StoreProvider>
      </themeContext.StoreProvider>
    )
  }
}

class Child extends React.Component {
  static contextType = [themeContext, userContext]
  render() {
    const [theme, user] = this.context

    /* ... */
  }
}

Qual seria a sua sintaxe ideal?

Use context.write, suporta o consumo de vários contextos sem aninhamento.

Consumir vários contextos sem aninhamento já é suportado. (Com ganchos.)

Context.write tem um RFC aberto para ele. Não sabemos se será aprovado porque levanta algumas questões muito complicadas. Mas, embora a RFC esteja aberta, não tenho certeza do que é acionável neste problema. Você tem algo a acrescentar além do que já está na motivação RFC?

Eu quero fazer uma pergunta. Por que não reage suporte consumindo vários contextos em classe? Parece que a API de ganchos tem muitos problemas a serem resolvidos e está muito instável no momento.😨🧐😥🤕

static contextType = [themeContext, userContext]
const [theme, user] = this.context

Vou implementar um StoreProviders que pode aninhar vários contextos automaticamente.

const StoreProviders = constructStoreProviders(...storeContexts)

<StoreProviders>
  <Child />
</StoreProviders>

Obrigado pela sua ajuda Dan! :)

@rabbitooops Quais exatamente os problemas que você tem com os ganchos? Eu uso ganchos na produção e eles funcionam bem para minha equipe.

E quanto a isso? É seguro usar o gancho agora. @gaearon

// A library
function useStoreProviders(Component, ...contexts) {
  contexts.forEach(context => Component.useProvider(context, someValue))
}

// User code
function App(props) {
  const [theme] = useState('white')

  // Safe to use `component hook`.
  App.useProvider(themeContext, theme)
  App.useShouldComponentUpdate(() => {})

  // Meanwhile, library can also use `component hook`.
  useStoreProviders(App, storeContext1, storeContext2, storeContext3)

  // Normal hook can't use `component hook`.
  customHook()

  /* ... */
}

<App />

decorateBeforeRender(App)

@rabbitooops Que tal usar uma única loja e um símbolo como chaves para imitar várias camadas de loja?

data Store = Leaf Object | C Store Store

Ou de forma imperfeita no 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})
}

@ TrySound Diferente: App.useProvider

@zhujinxuan Lamento, mas não consigo.

@zhujinxuan Você pode usar Não declarado , exemplo:

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

Parece que a API de ganchos tem muitos problemas a serem resolvidos e está muito instável no momento

Estamos preparando um lançamento dentro de uma ou duas semanas - não tenho certeza por que você inferiu isso. Eles estarão prontos em breve, mas se você quiser estar seguro, aguarde até uma versão estável.

E quanto a isso?

Chamar Ganchos em um loop (como você faz em forEach ) geralmente não é permitido. É fácil causar problemas dessa maneira.

useStoreProviders

Ambos useProvider e useShouldComponentUpdate são problemáticos como ganchos (é por isso que o React não os tem). Veja minha resposta em https://github.com/facebook/react/issues/14534#issuecomment -455411307.


No geral, estou lutando para entender a intenção desse problema.

O consumo de múltiplos contextos é resolvido por useContext Hook. Não é recomendável para de alguma forma "automatizar" com matrizes porque isso faz com que seja muito fácil de componentes de gravação que subscrevem a demasiada contexto e re-tornam demasiado frequentemente. Você deve ter uma noção clara de quais contextos um componente escuta, que é o que a API useContext oferece. Se necessário, você pode escrever useMyContexts() Hook que usa contextos específicos explicitamente. Só não recomendo torná-lo dinâmico como você fez, porque se o comprimento do array mudar, ele pode quebrar.

Colocar vários provedores pode ser visto como um "boilerplate" e, eventualmente, podemos ter uma solução para isso. Mas também não entendo por que você vê isso como um grande problema. Os exemplos neste tópico não são realistas o suficiente para explicar o problema para mim. Não vejo nada de ruim em aninhar várias camadas de JSX em algum lugar na parte superior do aplicativo. Tenho certeza de que você tem aninhamentos div muito mais profundos na maioria dos componentes e isso não prejudica muito.

Vou encerrar porque acho que já respondi a esses pontos e a discussão anda em círculos. Se houver algo faltando, me avise.

OT: @gaearon , existe algum plano para adicionar algo como useRender ou algo para ter mais controle de renderização? por exemplo:

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

O segundo argumento tem a mesma função de useEffect hook.

useMemo é seu amigo.

Veja o segundo snippet em https://reactjs.org/docs/hooks-faq.html#how -to-memoize-calculations.

Acabei com um código assim:

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;
}

Em seguida, em meu componente de fornecimento de nível superior:

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

Não vejo nada de ruim em aninhar várias camadas de JSX em algum lugar na parte superior do aplicativo.

Tenho cerca de 15 dependências que quero que sejam injetáveis ​​dessa maneira e ter 15 níveis de indentação não parece muito para mim :)

@ 0xorial você realmente não precisa ter um componente para isso, afinal <>> é apenas uma chamada de função React.createElement. Então, você pode simplificar para uma função de composição como:

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

e usá-lo como:

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/>
  );
}

Eu escrevi uma biblioteca no passado que lida com este caso: https://github.com/disjukr/join-react-context

react11

isso é definitivamente algo que acontece em aplicativos o tempo todo. useContext é ótimo para _consumir_ os dados contextuais em um componente, mas não é tão bom quando você precisa _fornecer_ contexto em um aplicativo com vários provedores.

Aqui está uma alternativa de fechamento da solução @alesmenzelsocialbakers :

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

A desvantagem é que você precisa escrever cada componente específico do Provedor.
Exemplo:

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

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

Eu criei uma biblioteca de gerenciamento de estado que é melhor na composição de serviços. Aqui está uma demonstração de como evitar o inferno do provedor . Sinta-se à vontade para experimentá-lo ou ler seu código-fonte (100 linhas de código)!

Ele introduz um objeto de "escopo" para coletar o provedor de contexto, de modo que:

  • Os serviços podem ser isolados ou compostos, dependendo se estão no mesmo escopo.

    • Os serviços podem consumir serviços anteriores no mesmo escopo, apesar de estarem no mesmo componente.

  • Todos os provedores coletados por um escopo podem ser fornecidos onece, evitando o inferno do provedor.

Eu tentei fazer isso também. Isso parece funcionar bem:

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

O uso é:

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

Eu também publiquei este auxiliar como uma biblioteca npm react-compose-wrappers

O seguinte mostra como estou passando o usuário autenticado para os componentes que precisam dele.

Decidi criar um estado para meu aplicativo. Em meu arquivo State.js, configurei meu estado inicial, contexto, redutor, provedor e gancho.

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;

Então, em meu arquivo index.js, envolvi meu aplicativo no provedor.

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

Para consumir o estado em um componente, posso usar o gancho. Também posso usar o envio para atualizar o estado. Por exemplo, se eu quiser obter ou definir um usuário.

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;

Acho que essa maneira me dá a liberdade de construir o estado como eu preciso sem adicionar uma tonelada de contextos extras e me ajuda a evitar um profundo ninho de provedores.

A próxima API Hooks fornece uma maneira diferente de consumir contextos.

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

Como faço para usar isso em um componente de classe?

A próxima API Hooks fornece uma maneira diferente de consumir contextos.
https://reactjs.org/docs/hooks-reference.html#usecontext

Como faço para usar isso em um componente de classe?

Os ganchos não são usados ​​para aproveitar os vários recursos do React sem escrever classes?
Bem, isto é, tudo que vários ganchos fazem já existe nas aulas. Se você está falando sobre sintaxe conveniente e API de uso, a reação passa das classes para componentes funcionais, então, bem-vindo às funções e ganchos

Criei um pacote para resolver o problema, fornecendo API semelhante com vue3

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

  • resolver o problema que o contexto de reação exige uma árvore de visão extra
  • também preserva a reatividade do que foi injetado
  • fractal entre provedores
  • use o WeakMap para armazenar as dependências
  • use o símbolo de objeto empacotado para obter melhor suporte de digitação, injeção de dependência e experiência de depuração

aviso prévio:

  • Não recomendo que você dependa muito da reatividade do contexto , porque é melhor fornecer o acesso aos dados do que aos próprios dados. O que @gaearon disse sobre não inscrever-se em muitos contextos estava certo. Mas ele não mencionou que é um padrão errado para resolver a assinatura de dados contando com a reatividade do contexto . Portanto, a coisa certa a fazer é não usar vários contextos, mas fornecer suas dependências em um contexto . E, enquanto isso, mantenha seus contextos o mais estáveis ​​possível .
  • E assim, se queremos uma API melhor, precisamos cuidar disso nós mesmos, e ter em mente que fazer a API fractal. É para isso que serve o meu pacote.

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>
    </>
  );
};

aqui está como eu faço isso:

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)
}

Você pode encontrar a função pipe em muitas bibliotecas populares, como rxjs, também há várias propostas de nível de linguagem para essa operação semelhante a pipeline. Não há necessidade de 'resolvê-lo' usando outra lib.

Esta página foi útil?
0 / 5 - 0 avaliações