Trabajar con devoluciones de llamada en React

Durante mi trabajo, me encontré periódicamente con el hecho de que los desarrolladores no siempre entienden claramente cómo funciona el mecanismo para transmitir datos a través de accesorios, en particular las devoluciones de llamada, y por qué sus PureComponents se actualizan con tanta frecuencia.


Por lo tanto, en este artículo entenderemos cómo se pasan las devoluciones de llamada a React y también discutiremos las características de los controladores de eventos.


TL; DR


  1. No interfiera con JSX y la lógica de negocios, esto complicará la percepción del código.
  2. Para pequeñas optimizaciones, las funciones del controlador de caché en forma de classProperties para clases o usando useCallback para funciones, entonces los componentes puros no se renderizarán constantemente. Especialmente el almacenamiento en caché de devolución de llamada puede ser útil para que cuando se pasan a PureComponent, no se producen ciclos de actualización innecesarios.
  3. No olvide que no obtiene un evento real en la devolución de llamada, sino un evento sintético. Si sale de la función actual, no podrá acceder a los campos de este evento. Almacena en caché los campos que necesitas si tienes cierres asincrónicos.

Parte 1. Controladores de eventos, almacenamiento en caché y percepción de código


React proporciona una forma bastante conveniente de agregar controladores de eventos para elementos html.


Esta es una de las cosas básicas que cualquier desarrollador debe saber cuando comienza a escribir en React:


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

Lo suficientemente simple? A partir de este código, queda claro de inmediato qué sucederá cuando el usuario haga clic en el botón.


Pero, ¿qué pasa si el código en el controlador se vuelve más y más?


Supongamos que hacemos clic en el botón para cargar y filtrar a todos los que no están en un equipo en particular ( user.team === 'search-team' ), luego los clasificamos por edad.


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

Este código es bastante difícil de entender. El código de lógica de negocios se mezcla con el diseño que ve el usuario.


La forma más fácil de deshacerse de esto: lleva la función al nivel de los métodos de clase:


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

Aquí cambiamos la lógica de negocios del código JSX a un campo separado en nuestra clase. Para hacer esto accesible dentro de la función, definimos la devolución de llamada de esta manera: onClick={() => this.fetchUsers()}


Además, al describir una clase, podemos declarar un campo como una función de flecha:


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

Esto nos permitirá declarar la devolución de llamada como onClick={this.fetchUsers}


¿Cuál es la diferencia entre estos dos métodos?


onClick={this.fetchUsers} - Aquí, con cada llamada a la función de renderizado en accesorios, el button siempre recibirá el mismo enlace.


En el caso de onClick={() => this.fetchUsers()} , cada vez que se llama a la función de representación, JavaScript inicializa una nueva función () => this.fetchUsers() y la establece en onClick prop. Esto significa que nextProp.onClick y prop.onClick en el button en este caso no siempre serán iguales, e incluso si el componente está marcado como limpio, se representará.


¿Qué amenaza esto con el desarrollo?


En la mayoría de los casos, no notará una reducción visual del rendimiento, porque el DOM virtual que generará el componente no será diferente del anterior, y no habrá cambios en su DOM.


Sin embargo, si representa grandes listas de componentes o tablas, notará "frenos" en una gran cantidad de datos.


¿Por qué es importante entender cómo se transfiere la función a la devolución de llamada?


A menudo, en Twitter o en stackoverflow, puede encontrar estos consejos:


"Si tiene problemas de rendimiento con las aplicaciones React, intente reemplazar la herencia de Component con PureComponent. Además, recuerde que para Component siempre puede definir shouldComponentUpdate para deshacerse de bucles de actualización innecesarios".


Si definimos un componente como puro, significa que ya tiene una función shouldComponentUpdate que hace shallowEqual entre props y nextProps.


Al pasar una nueva función de devolución de llamada a dicho componente cada vez, perdemos todas las ventajas y optimizaciones de PureComponent .


Veamos un ejemplo.
Cree un componente de entrada que también muestre información sobre cuántas veces se ha actualizado:


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

Creemos dos componentes que representarán la entrada 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> ); } } 

Y el 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> ); } } 

Puedes probar el ejemplo con tus manos aquí: https://codesandbox.io/s/2vwz6kjjkr
Este ejemplo demuestra cómo puede perder todos los beneficios de PureComponent si pasa una nueva función de devolución de llamada a PureComponent cada vez.


Parte 2. Uso de controladores de eventos en componentes de funciones


En la nueva versión de React (16.8), se anunció el mecanismo React Hooks , que le permite escribir componentes funcionales completos, con un ciclo de vida claro que puede cubrir casi todos los casos de usuarios que hasta ahora solo cubrían clases.


Modificamos el ejemplo con el componente Input para que todos los componentes estén representados por una función y trabajen con React-hooks.


La entrada debe almacenar dentro de sí misma información sobre cuántas veces se ha cambiado. Si en el caso de las clases usamos un campo en nuestra instancia, cuyo acceso se implementó a través de esto, entonces en el caso de una función no podremos declarar una variable a través de esto.
React proporciona un enlace useRef que se puede usar para guardar una referencia al HtmlElement en el árbol DOM, pero también es interesante porque puede usarse para datos regulares que nuestro componente necesita:


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

También necesitamos que el componente esté "limpio", es decir, se actualiza solo si los accesorios que se pasaron al componente han cambiado.
Para esto, hay diferentes bibliotecas que proporcionan HOC, pero es mejor usar la función memo, que ya está integrada en React, ya que funciona más rápido y de manera más eficiente:


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

El componente de entrada está listo, ahora reescribimos los componentes A y B.
En el caso del componente B, esto es fácil de hacer:


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

Aquí utilizamos el useState , que le permite guardar y trabajar con el estado del componente, en caso de que el componente esté representado por una función.


¿Cómo podemos almacenar en caché la función de devolución de llamada? No podemos eliminarlo del componente, ya que en este caso será común a diferentes instancias del componente.
Para tales tareas, React tiene un conjunto de ganchos de almacenamiento en caché y memoria, de los cuales useCallback es el más adecuado para useCallback https://reactjs.org/docs/hooks-reference.html


Agregue este gancho al 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> ); } 

Guardamos en caché la función, lo que significa que el componente de entrada no se actualizará cada vez.


¿Cómo useCallback gancho useCallback ?


Este enlace devuelve una función en caché (es decir, el enlace no cambia de render a render).
Además de la función que se va a almacenar en caché, se le pasa un segundo argumento: una matriz vacía.
Esta matriz le permite transferir una lista de campos, al cambiar los que necesita para cambiar la función, es decir. devolver un nuevo enlace.


useCallback ver la diferencia entre el método habitual de pasar una función a la devolución de llamada y useCallback aquí: https://codesandbox.io/s/0y7wm3pp1w


¿Por qué necesitamos una matriz?


Supongamos que necesitamos almacenar en caché una función que depende de algún valor a través de un cierre:


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

Aquí, el componente de la aplicación depende del accesorio a . Si ejecuta el ejemplo, todo funcionará correctamente hasta el momento en que agreguemos al final:


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

Después de que se active el tiempo de espera, cuando haga clic en el botón en alerta, se mostrará 1. Esto sucede porque guardamos la función anterior, que cerró a variable. Y dado que a es una variable, que en nuestro caso es un tipo de valor, y el tipo de valor es inmutable, obtuvimos este error. Si eliminamos el comentario /*a*/ , el código funcionará correctamente. Reaccionar en el segundo render verificará que los datos pasados ​​en la matriz son diferentes y devolverá una nueva función.


Puede probar este ejemplo usted mismo aquí: https://codesandbox.io/s/6vo8jny1ln


React proporciona muchas funciones que le permiten memorizar datos, como useRef , useCallback y useMemo .
Si este último es necesario para memorizar el valor de la función, y useCallback bastante similares a useRef , useRef permite almacenar en caché no solo referencias a elementos DOM, sino que también actúa como un campo de instancia.


A primera vista, se puede usar para almacenar en caché las funciones, porque useRef también almacena en caché los datos entre actualizaciones de componentes separadas.
Sin embargo, usar useRef para almacenar en caché las funciones no es deseable. Si nuestra función utiliza el cierre, en cualquier renderizado, el valor cerrado puede cambiar, y nuestra función en caché funcionará con el valor anterior. Esto significa que tendremos que escribir la lógica de actualización de la función o simplemente usar useCallback , en el que se implementa debido al mecanismo de dependencia.


https://codesandbox.io/s/p70pprpvvx aquí puede ver la memorización de funciones con el uso correcto de useCallback , con el incorrecto y con useRef .


Parte 3. Eventos sintéticos


Ya hemos descubierto cómo usar controladores de eventos y cómo trabajar correctamente con cierres en devoluciones de llamada, pero en React hay otra diferencia muy importante al trabajar con ellos:


Nota: ahora Input , con el que trabajamos anteriormente, es absolutamente sincrónico, pero en algunos casos puede ser necesario que la devolución de llamada se produzca con un retraso, de acuerdo con el patrón de rebote o aceleración . Entonces, debounce, por ejemplo, es muy conveniente de usar para la entrada de la cadena de búsqueda: la búsqueda solo ocurrirá cuando el usuario deje de escribir caracteres.


Cree un componente que internamente cause un cambio 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 no funcionará. El hecho es que React proxies eventos dentro de sí mismo, y el llamado Syntetic Event entra en nuestra devolución de llamada onChange, que después de nuestra función se "borrará" (los campos serán nulos). Por razones de rendimiento, React hace esto para usar un solo objeto, en lugar de crear uno nuevo cada vez.


Si necesitamos tomar valor, como en este ejemplo, es suficiente almacenar en caché los campos necesarios ANTES de salir de la función:


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

Puede ver un ejemplo aquí: https://codesandbox.io/s/oj6p8opq0z


En casos muy raros, se hace necesario mantener toda la instancia del evento. Para hacer esto, puede llamar a event.persist() , que elimina
esta instancia de evento sintético del grupo de eventos de eventos de reacción.


Conclusión


Los controladores de eventos React son muy convenientes ya que:


  1. Automatizar suscripción y cancelación de suscripción (con componente desmontable);
  2. Simplifique la percepción del código, la mayoría de las suscripciones son fáciles de rastrear en código JSX.

Pero al mismo tiempo, al desarrollar aplicaciones, puede encontrar algunas dificultades:


  1. Anular devoluciones de llamada en accesorios;
  2. Eventos sintéticos que se borran después de la ejecución de la función actual.

La anulación de devoluciones de llamada generalmente no se nota, ya que vDOM no cambia, pero vale la pena recordar que si introduce optimizaciones, reemplazando componentes con Pure a través de la herencia de PureComponent o utilizando memo , debe ocuparse de almacenarlos en caché, de lo contrario, los beneficios de introducir PureComponents o memo no serán notables. Para el almacenamiento en caché, puede usar classProperties (cuando trabaje con una clase) o useCallback hook (cuando trabaje con funciones).


Para una operación asincrónica correcta, si necesita datos de un evento, también guarde en caché los campos que necesita.

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


All Articles