Trabalhando com retornos de chamada no React

Durante meu trabalho, periodicamente me deparei com o fato de que os desenvolvedores nem sempre entendem claramente como o mecanismo de transmissão de dados por meio de adereços, em particular retornos de chamada, e por que seus PureComponents são atualizados com tanta frequência.


Portanto, neste artigo, entenderemos como os retornos de chamada são transmitidos para o React e também discutiremos os recursos dos manipuladores de eventos.


TL; DR


  1. Não interfira no JSX e na lógica de negócios - isso complicará a percepção do código.
  2. Para pequenas otimizações, as funções do manipulador de cache na forma de classProperties para classes ou usando useCallback para funções - os componentes puros não serão renderizados constantemente. Especialmente, o cache de retorno de chamada pode ser útil para que, quando passados ​​para o PureComponent, não ocorram ciclos de atualização desnecessários.
  3. Não esqueça que você não recebe um evento real no retorno de chamada, mas um evento sintético. Se você sair da função atual, não poderá acessar os campos deste evento. Coloque em cache os campos necessários se você tiver fechamentos assíncronos.

Parte 1. Manipuladores de eventos, cache e percepção de código


O React fornece uma maneira bastante conveniente de adicionar manipuladores de eventos para elementos html.


Essa é uma das coisas básicas que qualquer desenvolvedor conhece quando começa a escrever no React:


class MyComponent extends Component { render() { return <button onClick={() => console.log('Hello world!')}>Click me</button>; } } 

Simples o suficiente? A partir desse código, fica imediatamente claro o que acontecerá quando o usuário clicar no botão.


Mas e se o código no manipulador se tornar cada vez mais?


Suponha que, com um botão, tenhamos que carregar e filtrar todos os que não estão em uma equipe em particular ( user.team === 'search-team' ) e depois classificá-los por idade.


 class MyComponent extends Component { constructor(props) { super(props); this.state = { users: [] }; } render() { return ( <div> <ul> {this.state.users.map(user => ( <li>{user.name}</li> ))} </ul> <button onClick={() => { console.log('Hello world!'); window .fetch('/usersList') .then(result => result.json()) .then(data => { const users = data .filter(user => user.team === 'search-team') .sort((a, b) => { if (a.age > b.age) { return 1; } if (a.age < b.age) { return -1; } return 0; }); this.setState({ users: users, }); }); }} > Load users </button> </div> ); } } 

Esse código é muito difícil de descobrir. O código da lógica de negócios é combinado com o layout que o usuário vê.


A maneira mais fácil de se livrar disso é levar a função ao nível dos métodos de classe:


 class MyComponent extends Component { fetchUsers() { //    } render() { return ( <div> <ul> {this.state.users.map(user => ( <li>{user.name}</li> ))} </ul> <button onClick={() => this.fetchUsers()}>Load users</button> </div> ); } } 

Aqui, movemos a lógica de negócios do código JSX para um campo separado em nossa classe. Para tornar isso acessível dentro da função, definimos retorno de chamada da seguinte maneira: onClick={() => this.fetchUsers()}


Além disso, ao descrever uma classe, podemos declarar um campo como uma função de seta:


 class MyComponent extends Component { fetchUsers = () => { //    }; render() { return ( <div> <ul> {this.state.users.map(user => ( <li>{user.name}</li> ))} </ul> <button onClick={this.fetchUsers}>Load users</button> </div> ); } } 

Isso nos permitirá declarar o retorno de chamada como onClick={this.fetchUsers}


Qual é a diferença entre esses dois métodos?


onClick={this.fetchUsers} - Aqui, com todas as chamadas para a função render em props, o button sempre receberá o mesmo link.


No caso de onClick={() => this.fetchUsers()} , sempre que a função de renderização é chamada, o JavaScript inicializa uma nova função () => this.fetchUsers() e a configura para prop onClick . Isso significa que nextProp.onClick e prop.onClick no button nesse caso nem sempre serão iguais e, mesmo que o componente esteja marcado como limpo, ele será renderizado.


O que isso ameaça com o desenvolvimento?


Na maioria dos casos, você não notará uma redução de desempenho visualmente, porque o DOM Virtual que será gerado pelo componente não será diferente do anterior e não haverá alterações no seu DOM.


No entanto, se você renderizar grandes listas de componentes ou tabelas, notará "freios" em uma grande quantidade de dados.


Por que é importante entender como uma função é transferida para o retorno de chamada?


Muitas vezes, no twitter ou no stackoverflow, você pode encontrar essas dicas:


"Se você tiver problemas de desempenho com os aplicativos React, tente substituir a herança do Component pelo PureComponent. Além disso, lembre-se de que, para o Component, você sempre pode definir shouldComponentUpdate para se livrar de loops de atualização desnecessários".


Se definirmos um componente como Pure, isso significa que ele já possui uma função shouldComponentUpdate que faz o shallowEqual entre props e nextProps.


Ao passar uma nova função de retorno de chamada para esse componente a cada vez, perdemos todas as vantagens e otimizações do PureComponent .


Vejamos um exemplo.
Crie um componente de entrada que também exibirá informações sobre quantas vezes ele foi atualizado:


 class Input extends PureComponent { renderedCount = 0; render() { this.renderedCount++; return ( <div> <input onChange={this.props.onChange} /> <p>Input component was rerendered {this.renderedCount} times</p> </div> ); } } 

Vamos criar dois componentes que renderizarão o Input internamente:


 class A extends Component { state = { value: '' }; onChange = e => { this.setState({ value: e.target.value }); }; render() { return ( <div> <Input onChange={this.onChange} /> <p>The value is: {this.state.value} </p> </div> ); } } 

E o segundo:


 class B extends Component { state = { value: '' }; onChange(e) { this.setState({ value: e.target.value }); } render() { return ( <div> <Input onChange={e => this.onChange(e)} /> <p>The value is: {this.state.value} </p> </div> ); } } 

Você pode tentar o exemplo com as mãos aqui: https://codesandbox.io/s/2vwz6kjjkr
Este exemplo demonstra como você pode perder todos os benefícios do PureComponent se você passar uma nova função de retorno de chamada para o PureComponent a cada vez.


Parte 2. Usando manipuladores de eventos em componentes de função


Na nova versão do React (16.8), o mecanismo de ganchos do React foi anunciado, permitindo escrever componentes funcionais completos, com um ciclo de vida claro que pode cobrir quase todos os casos de usuários que até agora apenas cobriam classes.


Modificamos o exemplo com o componente Input para que todos os componentes sejam representados por uma função e trabalhem com ganchos de reação.


A entrada deve armazenar em si informações sobre quantas vezes foi alterada. Se no caso de classes usamos um campo em nossa instância, cujo acesso foi implementado por meio disso, no caso de uma função, não poderemos declarar uma variável por meio disso.
O React fornece um gancho useRef que pode ser usado para salvar uma referência ao HtmlElement na árvore DOM, mas também é interessante porque pode ser usado para dados regulares de que nosso componente precisa:


 import React, { useRef } from 'react'; export default function Input({ onChange }) { const componentRerenderedTimes = useRef(0); componentRerenderedTimes.current++; return ( <> <input onChange={onChange} /> <p>Input component was rerendered {componentRerenderedTimes.current} times</p> </> ); } 

Também precisamos que o componente esteja "limpo", ou seja, ele será atualizado apenas se os objetos que foram passados ​​para o componente tiverem sido alterados.
Para isso, existem bibliotecas diferentes que fornecem HOC, mas é melhor usar a função de memorando, que já está incorporada no React, pois funciona mais rápida e eficientemente:


 import React, { useRef, memo } from 'react'; export default memo(function Input({ onChange }) { const componentRerenderedTimes = useRef(0); componentRerenderedTimes.current++; return ( <> <input onChange={onChange} /> <p>Input component was rerendered {componentRerenderedTimes.current} times</p> </> ); }); 

O componente Input está pronto, agora reescrevemos os componentes A e B.
No caso do componente B, isso é fácil de fazer:


 import React, { useState } from 'react'; function B() { const [value, setValue] = useState(''); return ( <div> <Input onChange={e => setValue(e.target.value)} /> <p>The value is: {value} </p> </div> ); } 

Aqui usamos o gancho useState , que permite salvar e trabalhar com o estado do componente, caso o componente seja representado por uma função.


Como podemos armazenar em cache a função de retorno de chamada? Não podemos removê-lo do componente, pois nesse caso será comum a diferentes instâncias do componente.
Para essas tarefas, o React possui um conjunto de ganchos de armazenamento em cache e memorização, dos quais useCallback é o mais adequado para useCallback https://reactjs.org/docs/hooks-reference.html


Adicione este gancho ao componente A :


 import React, { useState, useCallback } from 'react'; function A() { const [value, setValue] = useState(''); const onChange = useCallback(e => setValue(e.target.value), []); return ( <div> <Input onChange={onChange} /> <p>The value is: {value} </p> </div> ); } 

Armazenamos em cache a função, o que significa que o componente Input não será atualizado todas as vezes.


Como o gancho useCallback funciona?


Esse gancho retorna uma função em cache (ou seja, o link não muda de renderização para renderização).
Além da função a ser armazenada em cache, um segundo argumento é passado a ela - uma matriz vazia.
Essa matriz permite transferir uma lista de campos, ao alterar a qual você precisa alterar a função, ou seja, retornar um novo link.


useCallback pode ver a diferença entre o método usual de transferir uma função para um retorno de chamada e useCallback aqui: https://codesandbox.io/s/0y7wm3pp1w


Por que precisamos de uma matriz?


Suponha que precisamos armazenar em cache uma função que depende de algum valor por meio de um fechamento:


 import React, { useCallback } from 'react'; import ReactDOM from 'react-dom'; import './styles.css'; function App({ a, text }) { const onClick = useCallback(e => alert(a), [ /*a*/ ]); return <button onClick={onClick}>{text}</button>; } const rootElement = document.getElementById('root'); ReactDOM.render(<App text={'Click me'} a={1} />, rootElement); 

Aqui, o componente App depende do suporte a . Se você executar o exemplo, tudo funcionará corretamente até o momento que adicionarmos ao final:


 setTimeout(() => ReactDOM.render(<App text={'Next A'} a={2} />, rootElement), 5000); 

Depois que o tempo limite for acionado, quando você clicar no botão em alerta, 1 será exibido. Isso acontece porque salvamos a função anterior, que fechou a variável. E como a é uma variável, que no nosso caso é um tipo de valor e o tipo de valor é imutável, obtivemos esse erro. Se removermos o comentário /*a*/ , o código funcionará corretamente. A reação na segunda renderização verificará se os dados passados ​​na matriz são diferentes e retornará uma nova função.


Você pode tentar este exemplo aqui: https://codesandbox.io/s/6vo8jny1ln


O React fornece muitas funções que permitem memorizar dados, como useRef , useCallback e useMemo .
Se o último for necessário para memorizar o valor da função e eles useCallback bastante semelhantes ao useRef , useRef permitirá que você useRef em cache não apenas referências a elementos DOM, mas também atue como um campo de instância.


À primeira vista, ele pode ser usado para armazenar em cache funções, porque useRef também armazena em cache dados entre atualizações de componentes separadas.
No entanto, o uso de useRef para armazenar em cache funções é indesejável. Se nossa função usa fechamento, em qualquer renderização, o valor fechado pode mudar e nossa função em cache funcionará com o valor antigo. Isso significa que precisaremos escrever a lógica de atualização da função ou apenas usar useCallback , na qual ela é implementada devido ao mecanismo de dependência.


https://codesandbox.io/s/p70pprpvvx aqui você pode ver a memorização de funções com o useCallback correto, com o errado e com useRef .


Parte 3. Eventos sintéticos


Já descobrimos como usar manipuladores de eventos e como trabalhar corretamente com fechamentos em retornos de chamada, mas no React há outra diferença muito importante ao trabalhar com eles:


Nota: agora o Input , com o qual trabalhamos acima, é absolutamente síncrono, mas em alguns casos pode ser necessário que o retorno de chamada ocorra com um atraso, de acordo com o padrão de rejeição ou limitação . Portanto, o debounce, por exemplo, é muito conveniente para a entrada da string de pesquisa - a pesquisa só acontece quando o usuário para de digitar caracteres.


Crie um componente que causa internamente uma alteração de estado:


 function SearchInput() { const [value, setValue] = useState(''); const timerHandler = useRef(); return ( <> <input defaultValue={value} onChange={e => { clearTimeout(timerHandler.current); timerHandler.current = setTimeout(() => { setValue(e.target.value); }, 300); // wait, if user is still writing his query }} /> <p>Search value is {value}</p> </> ); } 

Este código não irá funcionar. O fato é que o React proxies eventos dentro de si e o chamado Evento Sintético entra em nosso retorno de chamada onChange, que após nossa função será "limpo" (os campos serão nulos). Por motivos de desempenho, o React faz isso para usar um único objeto, em vez de criar um novo a cada vez.


Se precisarmos pegar valor, como neste exemplo, basta armazenar em cache os campos necessários ANTES de sair da função:


 function SearchInput() { const [value, setValue] = useState(''); const timerHandler = useRef(); return ( <> <input defaultValue={value} onChange={e => { clearTimeout(timerHandler.current); const pendingValue = e.target.value; // cached! timerHandler.current = setTimeout(() => { setValue(pendingValue); }, 300); // wait, if user is still writing his query }} /> <p>Search value is {value}</p> </> ); } 

Você pode ver um exemplo aqui: https://codesandbox.io/s/oj6p8opq0z


Em casos muito raros, torna-se necessário manter toda a instância do evento. Para fazer isso, você pode chamar event.persist() , que remove
esta instância do evento Syntetic do pool de eventos de eventos de reação.


Conclusão:


Os manipuladores de eventos React são muito convenientes, pois:


  1. Automatizar assinatura e cancelamento de assinatura (com componente desmontar);
  2. Simplifique a percepção do código, a maioria das assinaturas é fácil de rastrear no código JSX.

Mas, ao mesmo tempo, ao desenvolver aplicativos, você pode encontrar algumas dificuldades:


  1. Substituindo retornos de chamada em adereços;
  2. Eventos sintéticos que são limpos após a execução da função atual.

Substituir retornos de chamada geralmente não é perceptível, pois o vDOM não muda, mas vale lembrar que, se você introduzir otimizações, substituindo componentes pelo Pure por herança do PureComponent ou usando memo , deve-se cuidar de PureComponent lo em cache, caso contrário, os benefícios da introdução de PureComponents ou memorando não serão perceptíveis. Para armazenamento em cache, você pode usar classProperties (ao trabalhar com uma classe) ou useCallback hook (ao trabalhar com funções).


Para uma operação assíncrona correta, se você precisar de dados de um evento, também faça cache dos campos necessários.

Source: https://habr.com/ru/post/pt439138/


All Articles