React: Prevenindo rerenders com React.memo e useContext hook.

Criado em 19 mar. 2019  ·  37Comentários  ·  Fonte: facebook/react

Você quer solicitar um recurso ou relatar um bug ?

erro

Qual é o comportamento atual?

Não posso confiar nos dados da API de contexto usando (useContext hook) para evitar rerenders desnecessários com React.memo

Se o comportamento atual for um bug, forneça as etapas para reproduzir e, se possível, uma demonstração mínima do problema. Cole o link para o seu exemplo JSFiddle (https://jsfiddle.net/Luktwrdm/) ou CodeSandbox (https://codesandbox.io/s/new) abaixo:

React.memo(() => {
const [globalState] = useContext(SomeContext);

render ...

}, (prevProps, nextProps) => {

// How to rely on context in here?
// I need to rerender component only if globalState contains nextProps.value

});

Qual é o comportamento esperado?

Devo ter de alguma forma acesso ao contexto no retorno de chamada do segundo argumento React.memo para evitar a renderização
Ou devo ter a possibilidade de retornar uma instância antiga do componente de reação no corpo da função.

Quais versões do React e quais navegador / sistema operacional são afetados por esse problema?
16.8.4

Comentários muito úteis

Isso está funcionando conforme o planejado. Há uma discussão mais longa sobre isso em https://github.com/facebook/react/issues/14110 se você estiver curioso.

Digamos que por algum motivo você tenha AppContext cujo valor tem uma propriedade theme , e deseja apenas renderizar novamente algumas ExpensiveTree em appContextValue.theme alterações.

TLDR é que, por enquanto, você tem três opções:

Opção 1 (preferencial): Divida contextos que não mudam juntos

Se precisarmos apenas de appContextValue.theme em muitos componentes, mas o próprio appContextValue muda com muita frequência, poderíamos dividir ThemeContext de AppContext .

function Button() {
  let theme = useContext(ThemeContext);
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
}

Agora, qualquer mudança de AppContext não renderizará novamente ThemeContext consumidores.

Esta é a solução preferida. Então você não precisa de nenhum resgate especial.

Opção 2: Divida seu componente em dois, coloque memo entre

Se por algum motivo você não pode separar contextos, você ainda pode otimizar a renderização dividindo um componente em dois e passando acessórios mais específicos para o interno. Você ainda renderizaria o externo, mas deve ser barato, pois não faz nada.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"
  return <ThemedButton theme={theme} />
}

const ThemedButton = memo(({ theme }) => {
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
});

Opção 3: um componente com useMemo dentro

Finalmente, poderíamos tornar nosso código um pouco mais detalhado, mas mantê-lo em um único componente envolvendo o valor de retorno em useMemo e especificando suas dependências. Nosso componente ainda seria reexecutado, mas o React não renderizaria novamente a árvore filha se todas as entradas useMemo fossem iguais.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"

  return useMemo(() => {
    // The rest of your rendering logic
    return <ExpensiveTree className={theme} />;
  }, [theme])
}

Pode haver mais soluções no futuro, mas é o que temos agora.

Ainda assim, observe que a opção 1 é preferível - se algum contexto mudar com muita frequência, considere dividi-lo .

Todos 37 comentários

Isso está funcionando conforme o planejado. Há uma discussão mais longa sobre isso em https://github.com/facebook/react/issues/14110 se você estiver curioso.

Digamos que por algum motivo você tenha AppContext cujo valor tem uma propriedade theme , e deseja apenas renderizar novamente algumas ExpensiveTree em appContextValue.theme alterações.

TLDR é que, por enquanto, você tem três opções:

Opção 1 (preferencial): Divida contextos que não mudam juntos

Se precisarmos apenas de appContextValue.theme em muitos componentes, mas o próprio appContextValue muda com muita frequência, poderíamos dividir ThemeContext de AppContext .

function Button() {
  let theme = useContext(ThemeContext);
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
}

Agora, qualquer mudança de AppContext não renderizará novamente ThemeContext consumidores.

Esta é a solução preferida. Então você não precisa de nenhum resgate especial.

Opção 2: Divida seu componente em dois, coloque memo entre

Se por algum motivo você não pode separar contextos, você ainda pode otimizar a renderização dividindo um componente em dois e passando acessórios mais específicos para o interno. Você ainda renderizaria o externo, mas deve ser barato, pois não faz nada.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"
  return <ThemedButton theme={theme} />
}

const ThemedButton = memo(({ theme }) => {
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
});

Opção 3: um componente com useMemo dentro

Finalmente, poderíamos tornar nosso código um pouco mais detalhado, mas mantê-lo em um único componente envolvendo o valor de retorno em useMemo e especificando suas dependências. Nosso componente ainda seria reexecutado, mas o React não renderizaria novamente a árvore filha se todas as entradas useMemo fossem iguais.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"

  return useMemo(() => {
    // The rest of your rendering logic
    return <ExpensiveTree className={theme} />;
  }, [theme])
}

Pode haver mais soluções no futuro, mas é o que temos agora.

Ainda assim, observe que a opção 1 é preferível - se algum contexto mudar com muita frequência, considere dividi-lo .

Ambas as opções irão escapar da renderização dos filhos se o tema não tiver mudado.

@gaearon Os botões são filhos ou os botões são renderizados? Estou faltando algum contexto de como eles são usados.

Usar a opção 2 unstable_Profiler 2 ainda acionará onRender callbacks, mas não chamará a lógica de renderização real. Talvez eu esteja fazendo algo errado? ~ https://codesandbox.io/s/kxz4o2oyoo~ https://codesandbox.io/s/00yn9yqzjw

Eu atualizei o exemplo para ficar mais claro.

Usar a opção 2 do unstable_Profiler ainda acionará retornos de chamada onRender, mas não chamará a lógica de renderização real. Talvez eu esteja fazendo algo errado? https://codesandbox.io/s/kxz4o2oyoo

Esse é exatamente o objetivo dessa opção. :-)

Talvez uma boa solução para isso seja ter a possibilidade de "pegar" o contexto e renderizar novamente o componente apenas se o retorno de chamada for verdadeiro, por exemplo:
useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));

O principal problema com os ganchos que realmente encontrei é que não podemos gerenciar de dentro de um gancho o que é retornado por um componente - para evitar a renderização, retornar valor memorizado etc.

Se pudéssemos, não seria combinável.

https://overreacted.io/why-isnt-xa-hook/#not -a-hook-usebailout

Opção 4: não use contexto para propagação de dados, mas assinatura de dados. Use useSubscription (porque é difícil escrever para cobrir todos os casos).

Existe outra maneira de evitar a renderização novamente.
"Você precisa mover o JSX um nível acima do componente de re-renderização, então ele não será recriado a cada vez"

Mais informações aqui

Talvez uma boa solução para isso seja ter a possibilidade de "pegar" o contexto e renderizar novamente o componente apenas se o retorno de chamada for verdadeiro, por exemplo:
useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));

O principal problema com os ganchos que realmente encontrei é que não podemos gerenciar de dentro de um gancho o que é retornado por um componente - para evitar a renderização, retornar valor memorizado etc.

Em vez de verdadeiro / falso aqui ... poderíamos fornecer uma função baseada em identidade que nos permitisse subdividir os dados do contexto?

const contextDataINeed = useContext(ContextObj, (state) => state['keyICareAbout'])

onde useContext não apareceria neste componente, a menos que o resultado do seletor fn fosse diferente do resultado anterior da mesma função.

descobriram que essa biblioteca pode ser a solução para o Facebook se integrar aos ganchos https://blog.axlight.com/posts/super-performant-global-state-with-react-context-and-hooks/

Existe outra maneira de evitar a renderização novamente.
"Você precisa mover o JSX um nível acima do componente de re-renderização, então ele não será recriado a cada vez"

Mais informações aqui

O problema é que pode ser caro reestruturar a árvore de componentes apenas para evitar uma nova renderização de cima para baixo.

@fuleinist Em

@gaearon Não sei se estou faltando alguma coisa, mas tentei a sua segunda e terceira opções e elas não estão funcionando corretamente. Não tenho certeza se isso é apenas reagir ao bug da extensão do Chrome ou se há outro problema. Aqui está meu exemplo simples de formulário, onde vejo renderizar novamente as duas entradas. No console, vejo que o memo está fazendo seu trabalho, mas o DOM é renderizado o tempo todo. Eu tentei 1000 itens e o evento onChange é muito lento, é por isso que acho que memo () não está funcionando com o contexto corretamente. Obrigado por qualquer conselho:

Aqui está uma demonstração com 1000 itens / caixas de texto. Mas, nessa demonstração, as ferramentas de desenvolvimento não mostram uma nova renderização. Você deve baixar fontes locais para testá-lo: https://codesandbox.io/embed/zen-firefly-d5bxk

import React, { createContext, useState, useContext, memo } from "react";

const FormContext = createContext();

const FormProvider = ({ initialValues, children }) => {
  const [values, setValues] = useState(initialValues);

  const value = {
    values,
    setValues
  };

  return <FormContext.Provider value={value}>{children}</FormContext.Provider>;
};

const TextField = memo(
  ({ name, value, setValues }) => {
    console.log(name);
    return (
      <input
        type="text"
        value={value}
        onChange={e => {
          e.persist();
          setValues(prev => ({
            ...prev,
            [name]: e.target.value
          }));
        }}
      />
    );
  },
  (prev, next) => prev.value === next.value
);

const Field = ({ name }) => {
  const { values, setValues } = useContext(FormContext);

  const value = values[name];

  return <TextField name={name} value={value} setValues={setValues} />;
};

const App = () => (
  <FormProvider initialValues={{ firstName: "Marr", lastName: "Keri" }}>
    First name: <Field name="firstName" />
    <br />
    Last name: <Field name="lastName" />
  </FormProvider>
);

export default App;

image

Por outro lado, essa abordagem sem contexto funciona corretamente, ainda em depuração é mais lenta do que eu esperava, mas pelo menos re-renderizar está ok

import React, { useState, memo } from "react";
import ReactDOM from "react-dom";

const arr = [...Array(1000).keys()];

const TextField = memo(
  ({ index, value, onChange }) => (
    <input
      type="text"
      value={value}
      onChange={e => {
        console.log(index);
        onChange(index, e.target.value);
      }}
    />
  ),
  (prev, next) => prev.value === next.value
);

const App = () => {
  const [state, setState] = useState(arr.map(x => ({ name: x })));

  const onChange = (index, value) =>
    setState(prev => {
      return prev.map((item, i) => {
        if (i === index) return { name: value };

        return item;
      });
    });

  return state.map((item, i) => (
    <div key={i}>
      <TextField index={i} value={item.name} onChange={onChange} />
    </div>
  ));
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

image

@marrkeri Não vejo nada de errado no primeiro trecho de código. O componente destacado nas ferramentas de desenvolvimento é o Field que usa o contexto, não o TextField que é um componente memo e implementa a função areEqual.

Acho que o problema de desempenho no exemplo codesandbox vem dos 1000 componentes que usam o contexto. Refatore-o para um componente que usa o contexto, digamos Fields , e retorne desse componente (com um mapa) um TextField para cada valor.

@marrkeri Não vejo nada de errado no primeiro trecho de código. O componente destacado nas ferramentas de desenvolvimento é o Field que usa o contexto, não o TextField que é um componente memo e implementa a função areEqual.

Acho que o problema de desempenho no exemplo codesandbox vem dos 1000 componentes que usam o contexto. Refatore-o para um componente que usa o contexto, digamos Fields , e retorne desse componente (com um mapa) um TextField para cada valor.

Como você disse, eu estava pensando que deve ser renderizado novamente todas as vezes, mas ( ) somente quando o valor é alterado. Mas provavelmente são apenas problemas de ferramentas de desenvolvimento, porque adicionei preenchimento ao redor e em vez disso é renderizado novamente. Veja esta foto
image

image

Eu não entendi seu segundo ponto sobre a refatoração para um componente . Você poderia fazer um instantâneo, por favor? E o que vocês acham do número máximo exibido de quais estão ok sem atrasos? É de 1000 muito?

@marrkeri Eu estava sugerindo algo assim: https://codesandbox.io/s/little-night-p985y.

É esta a razão pela qual o react-redux teve que parar de usar os benefícios da API de contexto estável e passar o estado atual para o contexto quando eles migraram para os Ganchos?

Então parece que há

Opção 4: passar uma função de inscrição como valor de contexto, assim como na era do contexto legado

Pode ser a única opção se você não controlar os usos (necessário para as opções 2-3) e não puder enumerar todos os seletores possíveis (necessário para a opção 1), mas ainda deseja expor uma API Hooks

const MyContext = createContext()
export const Provider = ({children}) => (
  <MyContext.provider value={{subscribe: listener => ..., getValue: () => ...}}>
    {children}
  </MyContext.provider>
)

export const useSelector = (selector, equalityFunction = (a, b) => a === b) => {
  const {subscribe, getValue} = useContext(MyContext)
  const [value, setValue] = useState(getValue())
  useEffect(() => subscribe(state => {
      const newValue = selector(state)
      if (!equalityFunction(newValue, value) {
        setValue(newValue)
      }
  }), [selector, equalityFunction])
}

@Hypnosphi : paramos de passar o estado da loja no contexto (a implementação da v6) e voltamos para as assinaturas diretas da loja (a implementação da v7) devido a uma combinação de problemas de desempenho e a incapacidade de escapar das atualizações causadas pelo contexto (que o tornou impossível criar uma API de ganchos React-Redux baseada na abordagem v6).

Para mais detalhes, veja meu post A História e Implementação do React-Redux .

Eu li o tópico, mas ainda fico pensando - são as únicas opções disponíveis hoje para renderizar condicionalmente na mudança de contexto, "Opções 1 2 3 4" listadas acima? Há algo mais em andamento para resolver isso oficialmente ou as "4 soluções" são consideradas aceitáveis ​​o suficiente?

Eu escrevi no outro segmento, mas apenas no caso. Aqui está uma solução alternativa _não oficial_.
useContextSelector proposta e use-context-selector library no userland.

honestamente, isso me faz pensar que a estrutura simplesmente não está pronta para entrar totalmente em funções e ganchos como se estivéssemos sendo encorajados. Com classes e métodos de ciclo de vida você tinha uma maneira organizada de controlar essas coisas e agora com ganchos é uma sintaxe muito pior e menos legível

A opção 3 não parece funcionar. Estou fazendo algo errado? https://stackblitz.com/edit/react-w8gr8z

@Martin , não concordo com isso.

O padrão de gancho pode ser lido com documentação e código bem organizados
estrutura e classes React e ciclo de vida são todos substituíveis por
equivalentes.

Infelizmente, o padrão reativo não pode ser alcançado pelo React ao longo da
classes React ou ganchos.

No sábado, 11 de janeiro de 2020 às 9h44 Martin Genev [email protected]
escreveu:

honestamente, isso me faz pensar que a estrutura não está pronta para funcionar
totalmente em funções e ganchos como se estivéssemos sendo encorajados. Com aulas
e métodos de ciclo de vida, você tinha uma maneira organizada de controlar essas coisas e
agora com ganchos é uma sintaxe muito pior e menos legível

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/facebook/react/issues/15156?email_source=notifications&email_token=AAI4DWUJ7WUXMHAR6F2KVXTQ5D25TA5CNFSM4G7UEEO2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEIVN6OI#issuecomment-573235001 ,
ou cancelar
https://github.com/notifications/unsubscribe-auth/AAI4DWUCO7ORHV5OSDCE35TQ5D25TANCNFSM4G7UEEOQ
.

@mgenev Opção 3 impede a nova renderização do filho ( <ExpensiveTree /> , o nome fala por si)

@Hypnosphi obrigado, isso estava correto.
https://stackblitz.com/edit/react-ycfyye

Eu reescrevi e agora os componentes reais de renderização (exibição) não renderizam novamente, mas todos os contêineres (contexto conectado) renderizam em qualquer mudança de suporte no contexto, não importa se está em uso neles. Agora, a única opção que posso ver é começar a dividir os contextos, mas algumas coisas são realmente globais e envolvem no nível mais alto e uma mudança em qualquer suporte nelas fará com que todos os contêineres sejam acionados em todo o aplicativo, eu não não entendo como isso pode ser bom para o desempenho ...

Existe algum lugar onde haja um bom exemplo de como fazer isso com bom desempenho? Os documentos oficiais são realmente limitados

Você pode tentar minha opção 4, que é basicamente o que o react-redux faz internamente
https://github.com/facebook/react/issues/15156#issuecomment -546703046

Para implementar a função subscribe , você pode usar algo como Observables ou EventEmitter, ou apenas escrever uma lógica de assinatura básica você mesmo:

function StateProvider({children}) {
  const [state, dispatch] = useReducer(reducer, initialState)
  const listeners = useRef([])
  const subscribe = listener => {
    listeners.current.push(listener)
  }
  useEffect(() => {
    listeners.current.forEach(listener => listener(state)
  }, [state])

  return (
    <DispatchContext.Provider value={dispatch}>
      <SubscribeContext.Provider value={{subscribe, getValue: () => state}}>
          {children}      
      </SubscribeContext.Provider>
    </DispatchContext.Provider>
  );
}

Para aqueles que podem ter interesses, aqui está a comparação de várias bibliotecas um tanto relacionadas a este tópico.
https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode

Meu último esforço é verificar o suporte da ramificação de estado com useTransition que seria importante no próximo Modo Simultâneo.
Basicamente, se usarmos o estado e contexto React de uma forma normal, está tudo bem. (caso contrário, precisamos de um truque, por exemplo, este .)

Obrigado @ dai-shi, realmente gosto dos seus pacotes e estou pensando em adotá-los.

@ dai-shi oi, Acabei de encontrar seu react-tracked lib e parece muito bom se resolve problemas de desempenho com contextos como promete. Ainda é real ou melhor usar outra coisa? Aqui eu vejo um bom exemplo de seu uso também mostra como fazer o nível de middleware com use-reducer-async https://github.com/dai-shi/react-tracked/blob/master/examples/12_async/src/store .ts Obrigado por isso. Atualmente eu fiz algo semelhante usando useReducer , envolvendo dispatch com o meu próprio para middleware assíncrono e usando Context mas me preocupando com futuros problemas de desempenho de renderização devido ao envolvimento de contextos.

@bobrosoft react-tracked é bastante estável, eu diria (como um produto de desenvolvedor com certeza). O feedback é muito bem-vindo e é assim que uma biblioteca seria melhorada. Atualmente, ele usa internamente um recurso não documentado do React, e espero que possamos substituí-lo por um primitivo melhor no futuro. use-reducer-async é quase como um açúcar de sintaxe simples que nunca daria errado.

Este HoC funcionou para mim:
`` ``
import React, {useMemo, ReactElement, FC} de 'react';
importar reduzir de 'lodash / reduzir';

tipo Seletor = (contexto: qualquer) => qualquer;

interface SelectorObject {
}

const withContext = (
Componente: FC,
Contexto: qualquer,
seletores: SelectorObject,
): FC => {
return (props: any): ReactElement => {
const Consumer = ({context}: any): ReactElement => {
const contextProps = reduzir (
seletores,
(acc: any, selector: Selector, key: string): any => {
valor const = seletor (contexto);
acc [chave] = valor;
return acc;
},
{},
);
return useMemo (
(): ReactElement => ,
[... Object.values ​​(props), ... Object.values ​​(contextProps)],
);
};
Retorna (

{(contexto: qualquer): ReactElement => }

);
};
};

exportação padrão com Contexto;
`` ``

exemplo de uso:

export default withContext(Component, Context, { value: (context): any => context.inputs.foo.value, status: (context): any => context.inputs.foo.status, });

isso pode ser visto como o contexto equivalente de redux mapStateToProps

Fiz um hoc quase muito parecido com connect () no redux

const withContext = (
  context = createContext(),
  mapState,
  mapDispatchers
) => WrapperComponent => {
  function EnhancedComponent(props) {
    const targetContext = useContext(context);
    const { ...statePointers } = mapState(targetContext);
    const { ...dispatchPointers } = mapDispatchers(targetContext);
    return useMemo(
      () => (
        <WrapperComponent {...props} {...statePointers} {...dispatchPointers} />
      ),
      [
        ...Object.values(statePointers),
        ...Object.values(props),
        ...Object.values(dispatchPointers)
      ]
    );
  }
  return EnhancedComponent;
};

Implementação:

const mapActions = state => {
  return {};
};

const mapState = state => {
  return {
    theme: (state && state.theme) || ""
  };
};
export default connectContext(ThemeContext, mapState, mapActions)(Button);


Atualização: por fim, mudei para EventEmitter para dados granulares de mudança rápida com ouvintes dinâmicos (no movimento do mouse). Percebi que era a melhor ferramenta para o trabalho. O contexto é ótimo para compartilhar dados em geral, mas não em altas taxas de atualização.

Não se trata de uma assinatura declarativa ou não de um contexto? Encapsular condicionalmente um componente em outro que use useContext ().

O principal requisito é que o componente interno não possa ter estado, já que efetivamente será uma instância diferente por causa da ramificação. Ou talvez você possa pré-renderizar o componente e usar cloneElement para atualizar os adereços.

Alguns componentes não têm nada a ver com o contexto em sua renderização, mas precisam "apenas ler um valor" a partir dele. o que estou perdendo?
context

O Context._currentValue é seguro para uso na produção?

Estou tentando inscrever componentes em contextos que são tão baratos para renderizar quanto possível. Mas então me descobri passando adereços como antigamente com um código sphagetti ou usando memo para evitar a assinatura de atualizações quando não há nada lógico com as atualizações. As soluções de memorando são boas para quando a renderização de seu componente depende de contextos, mas do contrário ...

@ vamshi9666 você usa muito isso? você percebeu os prós / contras de sua abordagem? Eu gosto muito. Eu gosto da semelhança com redux, mas também como ele libera você para escrever gerenciamento de estado de aplicativo e lógica livremente como um contexto

Achei https://recoiljs.org/ uma boa solução para esse problema. Acho que seria incrível se você integrá-lo ao React.

@ vamshi9666 você usa muito isso? você percebeu os prós / contras de sua abordagem? Eu gosto muito. Eu gosto da semelhança com redux, mas também como ele libera você para escrever gerenciamento de estado de aplicativo e lógica livremente como um contexto

Usei-o apenas em um lugar e meu objetivo inicial é isolar o gerenciamento de estado do jsx, mas também não criar um não padrão. então, quando eu estendi a funcionalidade de mutações, parecia muito semelhante ao redutor do redux e ao padrão de criador de ação. O que é ainda melhor. mas não vejo motivo para reinventar algo, quando na verdade já existe.

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