Aguarde...

19 de março de 2019

Escrevendo Componentes Resilientes

Escrevendo Componentes Resilientes

Quando as pessoas começam a aprender Reagir, muitas vezes pedem um guia de estilo. Embora seja uma boa ideia ter algumas regras consistentes aplicadas em um projeto, muitas delas são arbitrárias – e, portanto, a React não tem uma opinião forte sobre elas.

Você pode usar sistemas de tipos diferentes, preferir declarações de função ou funções de seta, classificar seus objetos em ordem alfabética ou em uma ordem que achar agradável.

Essa flexibilidade permite integrar o React em projetos com convenções existentes. Mas também convida a debates intermináveis.

Não são princípios de design importantes que cada componente deve se esforçar para seguir. Mas eu não acho que os guias de estilo capturem bem esses princípios. Falaremos primeiro sobre guias de estilo e, em seguida, examinaremos os princípios que realmente são úteis .


Não se distraia com problemas imaginários

Antes de falarmos sobre princípios de design de componentes, quero dizer algumas palavras sobre guias de estilo. Esta não é uma opinião popular, mas alguém precisa dizer isso!

Na comunidade JavaScript, há alguns guias de estilo estritamente opinativos aplicados por um linter. Minha observação pessoal é que eles tendem a criar mais atrito do que eles valem. Eu não posso contar quantas vezes alguém me mostrou algum código absolutamente válido e disse: “Reagir reclama sobre isso”, mas foi a sua configuração lint reclamando! Isso leva a três problemas:

  • As pessoas se acostumam a ver o linter como um porteiro barulhento e com excesso de zelos, em vez de uma ferramenta útil. Avisos úteis são abafados por um mar de lêndeas de estilo. Como resultado, as pessoas não examinam as mensagens do linter durante a depuração e não recebem dicas úteis. Além disso, as pessoas menos habituadas a escrever JavaScript (por exemplo, designers) têm mais dificuldade em trabalhar com o código.
  • As pessoas não aprendem a diferenciar entre usos válidos e inválidos de um determinado padrão. Por exemplo, existe uma regra popular que proíbe a chamada setStateinterna componentDidMount. Mas se fosse sempre “ruim”, o React simplesmente não permitiria isso! Há um caso de uso legítimo para isso, e isso é medir o layout do nó DOM – por exemplo, para posicionar uma dica de ferramenta. Eu vi pessoas “contornar” esta regra, adicionando um setTimeoutque perde completamente o ponto.
  • Eventualmente, as pessoas adotam a “mentalidade de executor” e ficam opinadas sobre coisas que não trazem uma diferença significativa, mas são fáceis de verificar no código. “Você usou uma declaração de função, mas nossoprojeto usa funções de seta.” Sempre que tenho um forte sentimento de impor uma regra como essa, parecer mais profundo revela que eu investi esforço emocional nessa regra – e luto para deixá-la ir. Isso me leva a uma falsa sensação de realização sem melhorar meu código.

Eu estou dizendo que devemos parar de linting? De modo nenhum!

Com uma boa configuração, um linter é uma ótima ferramenta para capturar erros antes que eles aconteçam. Está se concentrando muito no estilo que o transforma em uma distração.


Marie Kondo Seu Lint Config

Aqui está o que eu sugiro que você faça na segunda-feira. Reúna sua equipe por meia hora, passe por todas as regras de lint ativadas na configuração do seu projeto e pergunte a si mesmo: “Essa regra já nos ajudou a detectar um bug?”Caso contrário, desligue-a. (Você também pode começar de uma lousa limpa com a eslint-config-react-appqual não há regras de estilo.)

No mínimo, sua equipe deve ter um processo para remover regras que causam atrito. Não assuma que qualquer coisa que você ou alguma outra pessoa tenha adicionado à sua configuração de lint há um ano é uma “melhor prática”. Questione e procure respostas. Não deixe ninguém lhe dizer que você não é inteligente o suficiente para escolher suas regras de fiapos.

Mas e a formatação? Use Prettier e esqueça as “lêndeas de estilo”. Você não precisa de uma ferramenta para gritar com você por colocar um espaço extra se outra ferramenta puder consertar isso para você. Use o linter para encontrar bugs , sem impor a estética .

Naturalmente, há aspectos do estilo de codificação que não estão diretamente relacionados à formatação, mas ainda podem ser irritantes quando inconsistentes em todo o projeto.

No entanto, muitos deles são muito sutis para capturar com uma regra de fiapos de qualquer maneira. É por isso que é importante criar confiança entre os membros da equipe e compartilhar aprendizados úteis na forma de uma página da wiki ou um pequeno guia de design.

Nem tudo vale a pena automatizar! Os insights obtidos a partir da leitura do raciocínio em tal guia podem ser mais valiosos do que seguir as “regras”.

Mas se seguir um guia de estilo estrito é uma distração, o que é realmente importante?

Esse é o assunto deste post.


Escrevendo Componentes Resilientes

Nenhuma quantidade de importações de indentação ou classificação em ordem alfabética pode corrigir um design quebrado. Então, ao invés de focar em como alguns códigos parecem , eu vou me concentrar em como isso funciona . Existem alguns princípios de design de componentes que eu acho muito úteis:

  1. Não pare o fluxo de dados
  2. Esteja sempre pronto para renderizar
  3. Nenhum componente é um singleton
  4. Mantenha o estado local isolado

Mesmo se você não usar o React, provavelmente descobrirá os mesmos princípios por tentativa e erro para qualquer modelo de componente de UI com fluxo de dados unidirecional.


Princípio 1: não pare o fluxo de dados

Não pare o fluxo de dados na renderização

Quando alguém usa seu componente, eles esperam que eles possam passar adereços diferentes a ele ao longo do tempo e que o componente reflita essas alterações:

// isOk might be driven by state and can change at any time
<Button color={isOk ? 'blue' : 'red'} />

Em geral, é assim que o React funciona por padrão. Se você usar um colorsuporte dentro de um Buttoncomponente, verá o valor fornecido acima para esse render:

function Button({ color, children }) {
  return (
    // ✅ `color` is always fresh!
    <button className={'Button-' + color}>
      {children}
    </button>
  );
}

No entanto, um erro comum ao aprender React é copiar props para o estado:

class Button extends React.Component {
  state = {
    color: this.props.color  };
  render() {
    const { color } = this.state; // 🔴 `color` is stale!    return (
      <button className={'Button-' + color}>
        {this.props.children}
      </button>
    );
  }
}

Isso pode parecer mais intuitivo no início se você usou classes fora do React. No entanto, ao copiar um suporte para o estado, você está ignorando todas as atualizações.

// 🔴 No longer works for updates with the above implementation
<Button color={isOk ? 'blue' : 'red'} />

Nos raros casos em que esse comportamento é intencional, certifique-se de chamar esse suporte initialColorou defaultColoresclarecer que as alterações nele são ignoradas.

Mas geralmente você vai querer ler os adereços diretamente no seu componente e evitar copiar adereços (ou qualquer coisa computada dos adereços) para o estado:

function Button({ color, children }) {
  return (
    // ✅ `color` is always fresh!
    <button className={'Button-' + color}>
      {children}
    </button>
  );
}

Valores computados são outra razão pela qual as pessoas às vezes tentam copiar adereços no estado. Por exemplo, imagine que determinamos a cor do texto do botão com base em uma computação cara com o plano de fundo colorcomo um argumento:

class Button extends React.Component {
  state = {
    textColor: slowlyCalculateTextColor(this.props.color)  };
  render() {
    return (
      <button className={
        'Button-' + this.props.color +
        ' Button-text-' + this.state.textColor // 🔴 Stale on `color` prop updates      }>
        {this.props.children}
      </button>
    );
  }
}

Este componente é buggy porque não recalcula this.state.textColorna coloralteração prop. A solução mais fácil seria mover o textColorcálculo para o rendermétodo e torná-lo um PureComponent:

class Button extends React.PureComponent {  render() {
    const textColor = slowlyCalculateTextColor(this.props.color);    return (
      <button className={
        'Button-' + this.props.color +
        ' Button-text-' + textColor // ✅ Always fresh
      }>
        {this.props.children}
      </button>
    );
  }
}

Problema resolvido! Agora, se os adereços mudarem, vamos recalcular textColor, mas evitamos o cálculo dispendioso dos mesmos adereços.

No entanto, podemos querer otimizá-lo ainda mais. E se for o childrenprop que mudou? Parece lamentável recalcular o textColornesse caso. Nossa segunda tentativa pode ser invocar o cálculo em componentDidUpdate:

class Button extends React.Component {
  state = {
    textColor: slowlyCalculateTextColor(this.props.color)
  };
  componentDidUpdate(prevProps) {    if (prevProps.color !== this.props.color) {      // 😔 Extra re-render for every update      this.setState({        textColor: slowlyCalculateTextColor(this.props.color),      });    }  }  render() {
    return (
      <button className={
        'Button-' + this.props.color +
        ' Button-text-' + this.state.textColor // ✅ Fresh on final render
      }>
        {this.props.children}
      </button>
    );
  }
}

No entanto, isso significaria que nosso componente faz uma segunda renderização após cada alteração. Isso também não é ideal se estamos tentando otimizá-lo.

Você poderia usar o componentWillReceivePropsciclo de vida legado para isso. No entanto, as pessoas costumam colocar efeitos colaterais lá também. Isso, por sua vez, costuma causar problemas para os próximos recursos derenderização simultânea , como o Time Slicing e o Suspense . E o getDerivedStateFromPropsmétodo “seguro” é desajeitado.

Vamos voltar por um segundo. Efetivamente, queremos a memorização . Temos algumas entradas e não queremos recalcular a saída a menos que as entradas sejam alteradas.

Com uma turma, você poderia usar um ajudante para a memorização. No entanto, os Ganchos levam isso um passo adiante, oferecendo uma maneira integrada de memorizar cálculos caros:

function Button({ color, children }) {
  const textColor = useMemo(    () => slowlyCalculateTextColor(color),    [color] // ✅ Don’t recalculate until `color` changes  );  return (
    <button className={'Button-' + color + ' Button-text-' + textColor}>
      {children}
    </button>
  );
}

Esse é todo o código que você precisa!

Em um componente de classe, você pode usar um auxiliar como memoize-onepara isso. Em um componente de função, o useMemoHook fornece uma funcionalidade semelhante.

Agora vemos que mesmo otimizar computações caras não é uma boa razão para copiar adereços no estado. Nosso resultado de renderização deve respeitar as mudanças nos adereços.


Não pare o fluxo de dados em efeitos colaterais

Até agora, falamos sobre como manter o resultado da renderização consistente com as alterações propostas. Evitar copiar suportes no estado é uma parte disso. No entanto, é importante que os efeitos colaterais (por exemplo, busca de dados) também façam parte do fluxo de dados .

Considere este componente React:

class SearchResults extends React.Component {
  state = {
    data: null
  };
  componentDidMount() {    this.fetchResults();  }  fetchResults() {
    const url = this.getFetchUrl();
    // Do the fetching...
  }
  getFetchUrl() {
    return 'http://myapi/results?query' + this.props.query;
  }
  render() {
    // ...
  }
}

Muitos componentes do React são assim – mas se olharmos um pouco mais de perto, notaremos um erro. O fetchResultsmétodo usa o queryprop para busca de dados:

  getFetchUrl() {
    return 'http://myapi/results?query' + this.props.query;  }

Mas e se o querysuporte mudar? Em nosso componente, nada vai acontecer. Isso significa que os efeitos colaterais do nosso componente não respeitam as alterações em seus adereços. Esta é uma fonte muito comum de erros em aplicativos React.

Para consertar nosso componente, precisamos:

  • Olhe componentDidMounte cada método chamado a partir dele.
    • No nosso exemplo, isso é fetchResultsgetFetchUrl.
  • Anote todos os adereços e estados usados ​​por esses métodos.
    • No nosso exemplo, isso é this.props.query.
  • Certifique-se de que sempre que esses adereços mudam, nós executamos novamente o efeito colateral.
    • Podemos fazer isso adicionando o componentDidUpdatemétodo.
class SearchResults extends React.Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.fetchResults();
  }
  componentDidUpdate(prevProps) {    if (prevProps.query !== this.props.query) { // ✅ Refetch on change      this.fetchResults();    }  }  fetchResults() {
    const url = this.getFetchUrl();
    // Do the fetching...
  }
  getFetchUrl() {
    return 'http://myapi/results?query' + this.props.query; // ✅ Updates are handled  }
  render() {
    // ...
  }
}

Agora nosso código respeita todas as mudanças nos objetos, mesmo para efeitos colaterais.

No entanto, é um desafio lembrar de não quebrá-lo novamente. Por exemplo, podemos adicionar currentPageao estado local e usá-lo em getFetchUrl:

class SearchResults extends React.Component {
  state = {
    data: null,
    currentPage: 0,  };
  componentDidMount() {
    this.fetchResults();
  }
  componentDidUpdate(prevProps) {
    if (prevProps.query !== this.props.query) {
      this.fetchResults();
    }
  }
  fetchResults() {
    const url = this.getFetchUrl();
    // Do the fetching...
  }
  getFetchUrl() {
    return (
      'http://myapi/results?query' + this.props.query +
      '&page=' + this.state.currentPage // 🔴 Updates are ignored    );
  }
  render() {
    // ...
  }
}

Infelizmente, nosso código está novamente com problemas porque o nosso efeito colateral não respeita as alterações currentPage.

Adereços e estado fazem parte do fluxo de dados React. A renderização e os efeitos colaterais devem refletir as alterações nesse fluxo de dados, não ignorá-los!

Para corrigir nosso código, podemos repetir as etapas acima:

  • Olhe componentDidMounte cada método chamado a partir dele.
    • No nosso exemplo, isso é fetchResultsgetFetchUrl.
  • Anote todos os adereços e estados usados ​​por esses métodos.
    • No nosso exemplo, isso é this.props.query ethis.state.currentPage .
  • Certifique-se de que sempre que esses adereços mudam, nós executamos novamente o efeito colateral.
    • Podemos fazer isso alterando o componentDidUpdatemétodo.

Vamos consertar nosso componente para lidar com atualizações para o currentPageestado:

class SearchResults extends React.Component {
  state = {
    data: null,
    currentPage: 0,
  };
  componentDidMount() {
    this.fetchResults();
  }
  componentDidUpdate(prevProps, prevState) {
    if (
      prevState.currentPage !== this.state.currentPage || // ✅ Refetch on change      prevProps.query !== this.props.query
    ) {
      this.fetchResults();
    }
  }
  fetchResults() {
    const url = this.getFetchUrl();
    // Do the fetching...
  }
  getFetchUrl() {
    return (
      'http://myapi/results?query' + this.props.query +
      '&page=' + this.state.currentPage // ✅ Updates are handled    );
  }
  render() {
    // ...
  }
}

Não seria bom se pudéssemos de alguma forma detectar automaticamente esses erros? Não é algo que um linter poderia nos ajudar?


Infelizmente, verificar automaticamente um componente de classe quanto à consistência é muito difícil. Qualquer método pode chamar qualquer outro método. Estaticamente, analisar chamadas de componentDidMountcomponentDidUpdateestá repleto de falsos positivos.

No entanto, pode-se projetar uma API que possa ser estaticamente analisada quanto à consistência. O React useEffectHook é um exemplo dessa API:

function SearchResults({ query }) {
  const [data, setData] = useState(null);
  const [currentPage, setCurrentPage] = useState(0);

  useEffect(() => {
    function fetchResults() {
      const url = getFetchUrl();
      // Do the fetching...
    }

    function getFetchUrl() {
      return (
        'http://myapi/results?query' + query +        '&page=' + currentPage      );
    }

    fetchResults();
  }, [currentPage, query]); // ✅ Refetch on change
  // ...
}

Colocamos a lógica dentro do efeito e isso facilita a visualização de quais valores do fluxo de dados React depende. Esses valores são chamados de “dependências” e, no nosso exemplo, são [currentPage, query].

Observe como essa matriz de “dependências de efeito” não é realmente um conceito novo. Em uma classe, tivemos que procurar por essas “dependências” por meio de todas as chamadas de método. A useEffectAPI apenas explicita o mesmo conceito.

Isso, por sua vez, nos permite validá-los automaticamente:

Escrevendo Componentes Resilientes

(Esta é uma demonstração da nova exhaustive-depsregra recomendada de lint que faz parte eslint-plugin-react-hooks. Ela será incluída em breve no Create React App.)

Observe que é importante respeitar todas as atualizações prop e de estado dos efeitos, independentemente de você estar escrevendo componente como uma classe ou uma função.

Com a API da classe, você precisa pensar na consistência e verificar se as alterações em cada props ou estados relevantes são tratadas por componentDidUpdate. Caso contrário, seu componente não é resiliente para prop e alterações de estado. Isso não é nem mesmo um problema específico do React. Aplica-se a qualquer biblioteca de interface do usuário que permite lidar com “criação” e “atualizações” separadamente.

useEffectAPI inverte o padrão, incentivando a consistência. Isso pode parecer estranho no começo , mas como resultado, seu componente se torna mais resiliente a mudanças na lógica. E como as “dependências” agora são explícitas, podemos verificar se o efeito é consistente usando uma regra de lint. Estamos usando um linter para pegar insetos!


Não pare o fluxo de dados em otimizações

Há mais um caso em que você pode ignorar acidentalmente as alterações nos adereços. Esse erro pode ocorrer quando você está otimizando manualmente seus componentes.

Observe que as abordagens de otimização que usam igualdade superficial como PureComponentReact.memocom a comparação padrão são seguras.

No entanto, se você tentar “otimizar” um componente escrevendo sua própria comparação, você pode se esquecer de comparar, por engano, a função adereços:

class Button extends React.Component {
  shouldComponentUpdate(prevProps) {    // 🔴 Doesn't compare this.props.onClick     return this.props.color !== prevProps.color;  }  render() {
    const onClick = this.props.onClick; // 🔴 Doesn't reflect updates    const textColor = slowlyCalculateTextColor(this.props.color);
    return (
      <button
        onClick={onClick}
        className={'Button-' + this.props.color + ' Button-text-' + textColor}>
        {this.props.children}
      </button>
    );
  }
}

É fácil perder esse erro no início porque, com as aulas, você normalmente passaria um método para baixo e, assim, teria a mesma identidade:

class MyForm extends React.Component {
  handleClick = () => { // ✅ Always the same function    // Do something  }  render() {
    return (
      <>
        <h1>Hello!</h1>
        <Button color='green' onClick={this.handleClick}>          Press me        </Button>      </>
    )
  }
}

Portanto, nossa otimização não se quebra imediatamente . No entanto, ele continuará “vendo” o onClickvalor antigo se mudar ao longo do tempo, mas outros adereços não:

class MyForm extends React.Component {
  state = {
    isEnabled: true
  };
  handleClick = () => {
    this.setState({ isEnabled: false });    // Do something
  }
  render() {
    return (
      <>
        <h1>Hello!</h1>
        <Button color='green' onClick={          // 🔴 Button ignores updates to the onClick prop          this.state.isEnabled ? this.handleClick : null        }>
          Press me
        </Button>
      </>
    )
  }
}

Neste exemplo, clicar no botão deve desativá-lo – mas isso não acontece porque o Buttoncomponente ignora as atualizações do onClickprop.

Isso pode ficar ainda mais confuso se a própria identidade da função depender de algo que pode mudar com o tempo, como draft.contentneste exemplo:

  drafts.map(draft =>
    <Button
      color='blue'
      key={draft.id}
      onClick={
        // 🔴 Button ignores updates to the onClick prop        this.handlePublish.bind(this, draft.content)      }>
      Publish
    </Button>
  )

Embora draft.contentpossa mudar com o tempo, nosso Buttoncomponente ignorou a mudança para o onClickprop, de modo que continue a ver a “primeira versão” do onClickmétodo vinculado com o original draft.content.

Então, como podemos evitar esse problema?

Eu recomendo evitar a implementação manual shouldComponentUpdatee evitar a especificação de uma comparação personalizada React.memo(). A comparação superficial padrão React.memorespeitará a alteração da identidade da função:

function Button({ onClick, color, children }) {
  const textColor = slowlyCalculateTextColor(this.props.color);
  return (
    <button
      onClick={onClick}
      className={'Button-' + color + ' Button-text-' + textColor}>
      {children}
    </button>
  );
}
export default React.memo(Button); // ✅ Uses shallow comparison

Em uma aula, PureComponenttem o mesmo comportamento.

Isso garante que passar uma função diferente como um prop sempre funcionará.

Se você insistir em uma comparação personalizada, certifique-se de não ignorar as funções:

  shouldComponentUpdate(prevProps) {
    // ✅ Compares this.props.onClick 
    return (
      this.props.color !== prevProps.color ||
      this.props.onClick !== prevProps.onClick    );
  }

Como mencionei anteriormente, é fácil perder esse problema em um componente de classe porque as identidades de método geralmente são estáveis ​​(mas nem sempre – e é aí que os bugs se tornam difíceis de depurar). Com Hooks, a situação é um pouco diferente:

  1. As funções são diferentes em cada renderização, então você descobre esse problema imediatamente .
  2. Com useCallbackuseContext, você pode evitar passar funções no fundo . Isso permite otimizar a renderização sem se preocupar com funções.

Para resumir esta seção, não pare o fluxo de dados!

Sempre que você usar adereços e estados, considere o que deve acontecer se eles mudarem. Na maioria dos casos, um componente não deve tratar a renderização inicial e atualiza de forma diferente. Isso faz com que resiliente às mudanças na lógica.

Com as classes, é fácil esquecer as atualizações ao usar adereços e estado dentro dos métodos do ciclo de vida. Ganchos te cutucam para fazer a coisa certa – mas é preciso algum ajuste mental se você não estiver acostumado a já fazer isso.


Princípio 2: Esteja sempre pronto para renderizar

Os componentes do React permitem escrever código de renderização sem se preocupar muito com o tempo. Você descreve como a interface do usuário deve se parecer em qualquer momento e o React faz isso acontecer. Aproveite esse modelo!

Não tente introduzir hipóteses de tempo desnecessárias no comportamento do seu componente. Seu componente deve estar pronto para renderizar novamente a qualquer momento.

Como alguém pode violar esse princípio? Reagir não facilita muito, mas você pode fazer isso usando o componentWillReceivePropsmétodo do ciclo de vida legado :

class TextInput extends React.Component {
  state = {
    value: ''
  };
  // 🔴 Resets local state on every parent render  componentWillReceiveProps(nextProps) {    this.setState({ value: nextProps.value });  }  handleChange = (e) => {
    this.setState({ value: e.target.value });
  };
  render() {
    return (
      <input
        value={this.state.value}
        onChange={this.handleChange}
      />
    );
  }
}

Neste exemplo, mantemos valueno estado local, mas também recebemos valuede adereços. Sempre que “recebemos novos adereços”, redefinimos o valueestado in.

O problema com esse padrão é que ele depende inteiramente do tempo acidental.

Talvez hoje o pai do componente atualize raramente, e assim nosso TextInputúnico “recebe adereços” quando algo importante acontece, como salvar um formulário.

Mas amanhã você pode adicionar alguma animação ao pai de TextInput. Se seu pai re-render mais vezes, ele continuará “explodindo” o estado da criança! Você pode ler mais sobre este problema em “Você provavelmente não precisa de um estado derivado” .

Então, como podemos consertar isso?

Primeiro de tudo, precisamos consertar nosso modelo mental. Precisamos parar de pensar em “receber adereços” como algo diferente de apenas “renderização”. Um novo processamento causado por um pai não deve se comportar de maneira diferente de um novo processamento causado por nossa própria alteração de estado local. Os componentes devem ser resilientes à renderização com menos ou mais frequência, porque, caso contrário, eles são muito acoplados a seus pais em particular.

Esta demonstração mostra como a nova renderização pode quebrar componentes frágeis.)

Embora existam algumas soluções diferentes para quando você realmente quer derivar o estado de adereços, geralmente você deve usar um componente totalmente controlado:

// Option 1: Fully controlled component.
function TextInput({ value, onChange }) {
  return (
    <input
      value={value}
      onChange={onChange}
    />
  );
}

Ou você pode usar um componente não controlado com uma chave para redefini-lo:

// Option 2: Fully uncontrolled component.
function TextInput() {
  const [value, setValue] = useState('');
  return (
    <input
      value={value}
      onChange={e => setValue(e.target.value)}
    />
  );
}

// We can reset its internal state later by changing the key:
<TextInput key={formId} />

O detalhe desta seção é que seu componente não deve quebrar apenas porque ele ou seu pai re-renderiza mais frequentemente. O design da React API facilita o processo de evitar o componentWillReceivePropsmétodo do ciclo de vida legado .

Para testar seu componente com ênfase, você pode adicionar temporariamente esse código ao pai:

componentDidMount() {
  // Don't forget to remove this immediately!  setInterval(() => this.forceUpdate(), 100);
}

Não deixe este código – é apenas uma maneira rápida de verificar o que acontece quando um pai processa novamente com mais frequência do que o esperado. Não deveria quebrar a criança!


Você pode estar pensando: “Eu vou continuar a reconfigurar o estado quando os adereços mudarem, mas evitará re-renderizações desnecessárias PureComponent”.

Esse código deve funcionar, certo?

// 🤔 Should prevent unnecessary re-renders... right?class TextInput extends React.PureComponent {  state = {
    value: ''
  };
  // 🔴 Resets local state on every parent render
  componentWillReceiveProps(nextProps) {
    this.setState({ value: nextProps.value });
  }
  handleChange = (e) => {
    this.setState({ value: e.target.value });
  };
  render() {
    return (
      <input
        value={this.state.value}
        onChange={this.handleChange}
      />
    );
  }
}

A princípio, pode parecer que esse componente resolve o problema de “explodir” o estado na re-renderização pai. Afinal, se os adereços são os mesmos, nós simplesmente pulamos a atualização – e assim componentWillReceivePropsnão somos chamados.

No entanto, isso nos dá uma falsa sensação de segurança. Este componente ainda não é resiliente às mudanças de sustentação reais . Por exemplo, se adicionássemos outro objeto de alteração frequente, como um animado style, ainda “perderíamos” o estado interno:

<TextInput
  style={{opacity: someValueFromState}}  value={
    // 🔴 componentWillReceiveProps in TextInput
    // resets to this value on every animation tick.
    value
  }
/>

Portanto, essa abordagem ainda é falha. Podemos ver que várias otimizações gosto PureComponentshouldComponentUpdateReact.memonão deve ser usado para controlar o comportamento . Use-os apenas para melhorar o desempenhoquando isso ajudar. Se a remoção de uma otimização quebra um componente, ela é muito frágil para começar.

A solução aqui é a mesma que descrevemos anteriormente. Não trate “recebendo adereços” como um evento especial. Evite “sincronizar” adereços e estado. Na maioria dos casos, todos os valores devem ser totalmente controlados (por meio de adereços) ou totalmente descontrolados (no estado local). Evite o estado derivado quando puder . E esteja sempre pronto para renderizar!


Princípio 3: Nenhum componente é um singleton

Às vezes, assumimos que um determinado componente só é exibido uma vez. Tal como uma barra de navegação. Isso pode ser verdade por algum tempo. No entanto, essa suposição geralmente causa problemas de design que surgem apenas muito mais tarde.

Por exemplo, talvez você precise implementar uma animação entre dois Pagecomponentes em uma alteração de rota – a anterior Pagee a próxima Page. Ambos precisam ser montados durante a animação. No entanto, você pode descobrir que cada um desses componentes assume que é o único Pagena tela.

É fácil verificar esses problemas. Apenas por diversão, tente renderizar seu aplicativo duas vezes:

ReactDOM.render(
  <>
    <MyApp />    <MyApp />  </>,
  document.getElementById('root')
);

Clique ao redor. (Você pode precisar ajustar algumas CSS para este experimento.)

Seu aplicativo ainda se comporta como esperado? Ou você vê estranhas falhas e erros? É uma boa ideia fazer esse teste de estresse em componentes complexos de vez em quando e garantir que várias cópias deles não entrem em conflito entre si.

Um exemplo de um padrão problemático que eu mesmo escrevi algumas vezes está realizando “limpeza” de estado global em componentWillUnmount:

componentWillUnmount() {
  // Resets something in Redux store  this.props.resetForm();}

Naturalmente, se houver dois desses componentes na página, desmontar um deles pode quebrar o outro. Redefinir o estado “global” na montagem não é melhor:

componentDidMount() {
  // Resets something in Redux store  this.props.resetForm();}

Nesse caso, montar uma segunda forma quebrará a primeira.

Esses padrões são bons indicadores de onde nossos componentes são frágeis. Mostrar ou ocultar uma árvore não deve quebrar componentes fora dessa árvore.

Se você pretende renderizar esse componente duas vezes ou não, a solução desses problemas é compensada a longo prazo. Isso leva você a um design mais resiliente.


Princípio 4: Mantenha o Estado local isolado

Considere um Postcomponente de mídia social . Tem uma lista de Commentthreads (que podem ser expandidos) e uma NewCommententrada.

Reagir componentes podem ter estado local. Mas qual estado é verdadeiramente local? O conteúdo da postagem é estado local ou não? E a lista de comentários? Ou o registro de quais segmentos de comentários são expandidos? Ou o valor da entrada de comentários?

Se você está acostumado a colocar tudo em um “gerente de estado”, responder a essa pergunta pode ser um desafio. Então aqui está uma maneira simples de decidir.

Se você não tiver certeza se algum estado é local, pergunte a si mesmo: “Se esse componente foi renderizado duas vezes, essa interação deve refletir na outra cópia?” Sempre que a resposta for “não”, você encontrará algum estado local.

Por exemplo, imagine que renderizamos o mesmo Postduas vezes. Vamos olhar para coisas diferentes que podem mudar.

  • Publicar conteúdo. Nós queremos editar a postagem em uma árvore para atualizá-la em outra árvore. Portanto, provavelmente não deve ser o estado local de um Postcomponente. (Em vez disso, o conteúdo da postagem poderia estar em algum cache, como o Apollo, o Relay ou o Redux.)
  • Lista de comentários Isso é semelhante ao postar conteúdo. Nós gostaríamos de adicionar um novo comentário em uma árvore para ser refletido na outra árvore também. Então, idealmente, nós usaríamos algum tipo de cache para isso, e não deveria ser um estado local do nosso Post.
  • Quais comentários são expandidos. Seria estranho se expandir um comentário em uma árvore também o expandisse em outra árvore. Nesse caso, estamos interagindo com uma Comment representação de interface do usuário específica, em vez de uma “entidade de comentário” abstrata. Portanto, um sinalizador “expandido” deve ser um estado local do arquivo Comment.
  • O valor da nova entrada de comentário. Seria estranho se digitar um comentário em uma entrada também atualizasse uma entrada em outra árvore. A menos que os insumos sejam claramente agrupados, geralmente as pessoas esperam que eles sejam independentes. Portanto, o valor de entrada deve ser um estado local do NewCommentcomponente.

Eu não sugiro uma interpretação dogmática dessas regras. É claro que, em um aplicativo mais simples, você pode querer usar o estado local para tudo, incluindo os “caches”. Estou falando apenas da experiência do usuário ideal dos primeiros princípios .

Evite tornar o estado verdadeiramente local global. Isso entra no nosso tópico de “resiliência”: há menos sincronização surpreendente acontecendo entre os componentes. Como bônus, isso também corrige uma grande classe de problemas de desempenho. “Over-rendering” é muito menos um problema quando o seu estado está no lugar certo.

Postado em BlogTags:
Escreva um comentário