Rastrear por que um componente React está sendo renderizado novamente

156

Existe uma abordagem sistemática para depurar o que está fazendo com que um componente seja renderizado novamente no React? Coloquei um console.log () simples para ver quanto tempo ele renderiza, mas estou tendo problemas para descobrir o que está causando o componente renderizar várias vezes, ou seja, (4 vezes) no meu caso. Existe uma ferramenta que mostra uma linha do tempo e / ou todos os processamentos e pedidos da árvore de componentes?

jasan
fonte
Talvez você possa usar shouldComponentUpdatepara desativar a atualização automática de componentes e iniciar o rastreamento a partir daí. Mais informações podem ser encontradas aqui: facebook.github.io/react/docs/optimizing-performance.html
Reza Sadr
A resposta de @jpdelatorre está correta. Em geral, um dos pontos fortes do React é que você pode rastrear facilmente o fluxo de dados de volta à cadeia, observando o código. A extensão React DevTools pode ajudar com isso. Além disso, tenho uma lista de ferramentas úteis para a visualização / rastreamento Reagir componente re-processamento como parte do meu catálogo complementos Redux , e uma série de artigos sobre [Reagir desempenho monitoramento] (htt
markerikson

Respostas:

253

Se você quiser um pequeno trecho sem nenhuma dependência externa, acho útil

componentDidUpdate(prevProps, prevState) {
  Object.entries(this.props).forEach(([key, val]) =>
    prevProps[key] !== val && console.log(`Prop '${key}' changed`)
  );
  if (this.state) {
    Object.entries(this.state).forEach(([key, val]) =>
      prevState[key] !== val && console.log(`State '${key}' changed`)
    );
  }
}

Aqui está um pequeno gancho que eu uso para rastrear atualizações dos componentes funcionais

function useTraceUpdate(props) {
  const prev = useRef(props);
  useEffect(() => {
    const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
      if (prev.current[k] !== v) {
        ps[k] = [prev.current[k], v];
      }
      return ps;
    }, {});
    if (Object.keys(changedProps).length > 0) {
      console.log('Changed props:', changedProps);
    }
    prev.current = props;
  });
}

// Usage
function MyComponent(props) {
  useTraceUpdate(props);
  return <div>{props.children}</div>;
}
Jacob Rask
fonte
5
@ yarden.refaeli Não vejo razão para ter um bloco if. Curto e conciso.
Isaac Isaac
Junto com isso, se você encontrar uma parte do estado que está sendo atualizada e não for óbvio onde ou por quê, poderá substituir o setStatemétodo (em um componente de classe) por setState(...args) { super.setState(...args) }e, em seguida, definir um ponto de interrupção no depurador, que poderá para rastrear de volta para a função que define o estado.
Redbmk
Como exatamente eu uso a função de gancho? Para onde exatamente devo ligar useTraceUpdatedepois de defini-lo como você o escreveu?
Damon
Em um componente de função, você pode usá-lo como este function MyComponent(props) { useTraceUpdate(props); }e ele irá registrar sempre que adereços mudanças
Jacob Rask
1
@ DawsonB, você provavelmente não possui nenhum estado nesse componente, por isso this.stateé indefinido.
Jacob Rask
67

Aqui estão algumas instâncias em que um componente React será renderizado novamente.

  • Re-renderizador do componente pai
  • Chamando this.setState()dentro do componente. Isso irá provocar os seguintes métodos de componentes de ciclo de vida shouldComponentUpdate> componentWillUpdate> render>componentDidUpdate
  • Mudanças nos componentes props. Esse gatilho vontade componentWillReceiveProps> shouldComponentUpdate> componentWillUpdate> render> componentDidUpdate( connectmétodo de react-reduxgatilho isso quando há mudanças aplicáveis na loja Redux)
  • chamando this.forceUpdateque é semelhante athis.setState

Você pode minimizar o renderizador do seu componente implementando uma verificação dentro do seu shouldComponentUpdatee retornando falsese não for necessário.

Outra maneira é usar React.PureComponent ou componentes sem estado. Os componentes puros e sem estado são renderizados novamente somente quando houver alterações nos acessórios.

jpdelatorre
fonte
6
Nitpick: "sem estado" significa apenas qualquer componente que não use estado, seja ele definido com sintaxe de classe ou sintaxe funcional. Além disso, os componentes funcionais sempre são renderizados novamente. Você precisa usar shouldComponentUpdate, ou estender React.PureComponent, para aplicar somente a nova renderização nas alterações.
markerikson
1
Você está certo sobre o componente apátrida / funcional sempre é renderizado novamente. Atualizará minha resposta.
jpdelatorre
você pode esclarecer, quando e por que os componentes funcionais sempre são renderizados novamente? Eu uso um pouco de componentes funcionais no meu aplicativo.
Jasan
Portanto, mesmo se você usar a maneira funcional de criar seu componente, por exemplo const MyComponent = (props) => <h1>Hello {props.name}</h1>;(esse é um componente sem estado). Ele será renderizado novamente quando o componente pai for renderizado novamente.
Jspdelatorre
2
Esta é uma ótima resposta, com certeza, mas não responde à pergunta real: - Como rastrear o que desencadeou uma re-renderização. A resposta de Jacob R parece promissora em dar a resposta ao problema real.
Sanuj 19/02/19
10

A resposta do @ jpdelatorre é ótima para destacar razões gerais pelas quais um componente React pode ser renderizado novamente.

Eu só queria mergulhar um pouco mais fundo em uma instância: quando os objetos mudam . A solução de problemas do que está causando a re-renderização de um componente React é um problema comum e, na minha experiência, muitas vezes rastrear esse problema envolve determinar quais objetos estão sendo alterados .

Os componentes de reação são renderizados novamente quando recebem novos adereços. Eles podem receber novos adereços como:

<MyComponent prop1={currentPosition} prop2={myVariable} />

ou se MyComponentestiver conectado a uma loja redux:

function mapStateToProps (state) {
  return {
    prop3: state.data.get('savedName'),
    prop4: state.data.get('userCount')
  }
}

Sempre que o valor de prop1, prop2, prop3, ou prop4mudanças MyComponentvai voltar a render. Com 4 adereços, não é muito difícil rastrear quais adereços estão sendo alterados colocando um console.log(this.props)no início do renderbloco. No entanto, com componentes mais complicados e mais e mais adereços, esse método é insustentável.

Aqui está uma abordagem útil (usando o lodash por conveniência) para determinar quais alterações de prop estão causando a re-renderização de um componente:

componentWillReceiveProps (nextProps) {
  const changedProps = _.reduce(this.props, function (result, value, key) {
    return _.isEqual(value, nextProps[key])
      ? result
      : result.concat(key)
  }, [])
  console.log('changedProps: ', changedProps)
}

A adição desse snippet ao seu componente pode ajudar a revelar o culpado que está causando as renderizações duvidosas e, muitas vezes, isso ajuda a esclarecer os dados desnecessários sendo canalizados para os componentes.

Cumulo Nimbus
fonte
3
Agora é chamado UNSAFE_componentWillReceiveProps(nextProps)e está obsoleto. "Este ciclo de vida foi nomeado anteriormente componentWillReceiveProps. Esse nome continuará funcionando até a versão 17." Na documentação do React .
Emile Bergeron
1
Você pode conseguir o mesmo com o componentDidUpdate, que é sem dúvida melhor de qualquer maneira, pois você só quer descobrir o que causou a atualização de um componente.
ver mais nítida
5

Estranho, ninguém deu essa resposta, mas eu a acho muito útil, principalmente porque as mudanças nos objetos estão quase sempre profundamente aninhadas.

Hooks fanboys:

import deep_diff from "deep-diff";
const withPropsChecker = WrappedComponent => {
  return props => {
    const prevProps = useRef(props);
    useEffect(() => {
      const diff = deep_diff.diff(prevProps.current, props);
      if (diff) {
        console.log(diff);
      }
      prevProps.current = props;
    });
    return <WrappedComponent {...props} />;
  };
};

Fanboys "velhos" da escola:

import deep_diff from "deep-diff";
componentDidUpdate(prevProps, prevState) {
      const diff = deep_diff.diff(prevProps, this.props);
      if (diff) {
        console.log(diff);
      }
}

PS: Eu ainda prefiro usar HOC (componente de ordem superior) porque às vezes você destrói seus adereços no topo e a solução de Jacob não se encaixa bem

Isenção de responsabilidade: nenhuma afiliação com o proprietário do pacote. Basta clicar em dezenas de vezes para tentar identificar a diferença em objetos profundamente aninhados.

ZenVentzi
fonte
4

Agora existe um gancho para isso disponível no npm:

https://www.npmjs.com/package/use-trace-update

(Divulgação, publiquei) Atualização: Desenvolvida com base no código de Jacob Rask

Damian Green
fonte
14
Este é praticamente o mesmo código que Jacob postou. Poderia ter creditado ele lá.
Christian Ivicevic 23/09/19
2

Usando ganchos e componentes funcionais, não apenas a mudança de suporte pode causar uma nova renderização. O que comecei a usar é um log bastante manual. Eu me ajudei muito. Você pode achar útil também.

Eu colo esta parte no arquivo do componente:

const keys = {};
const checkDep = (map, key, ref, extra) => {
  if (keys[key] === undefined) {
    keys[key] = {key: key};
    return;
  }
  const stored = map.current.get(keys[key]);

  if (stored === undefined) {
    map.current.set(keys[key], ref);
  } else if (ref !== stored) {
    console.log(
      'Ref ' + keys[key].key + ' changed',
      extra ?? '',
      JSON.stringify({stored}).substring(0, 45),
      JSON.stringify({now: ref}).substring(0, 45),
    );
    map.current.set(keys[key], ref);
  }
};

No início do método, mantenho uma referência ao WeakMap:

const refs = useRef(new WeakMap());

Depois de cada chamada "suspeita" (adereços, ganchos), escrevo:

const example = useExampleHook();
checkDep(refs, 'example ', example);
Miklos Jakab
fonte
1

As respostas acima são muito úteis, caso alguém esteja procurando um método específico para detectar a causa do novo renderizador, achei essa biblioteca redux-logger muito útil.

O que você pode fazer é adicionar a biblioteca e ativar a diferença entre os estados (existe nos documentos), como:

const logger = createLogger({
    diff: true,
});

E adicione o middleware na loja.

Em seguida, coloque um console.log()na função de renderização do componente que você deseja testar.

Em seguida, você pode executar o aplicativo e verificar os logs do console. Sempre que houver um log antes de mostrar a diferença entre o estado (nextProps and this.props)e você pode decidir se a renderização é realmente necessária láinsira a descrição da imagem aqui

Será semelhante à imagem acima, juntamente com a tecla diff.

pritesh
fonte