Cómo trabajar con React de la manera correcta para evitar algunos errores comunes

Teclado Macbook Pro

Una cosa que escucho con bastante frecuencia es " Vamos por Redux " en nuestra nueva aplicación React. Le ayuda a escalar, y los datos de la aplicación no deberían estar en el estado local de React porque es ineficiente. O cuando llama a una API y mientras la promesa está pendiente, el componente se desmonta y obtiene el siguiente hermoso error.

Advertencia: No se puede llamar a setState (o forceUpdate) en un componente desmontado. Esta es una operación no operativa, pero indica una pérdida de memoria en su aplicación. Para solucionarlo, cancele todas las suscripciones y tareas asincrónicas en el método componentWillUnmount.

Entonces, la solución a la que la gente suele llegar es usar Redux .¡Amo Redux y el trabajo que está haciendo Dan Abramov es simplemente increíble! Ese tipo es genial, desearía tener la mitad de talento que él.

Pero estoy seguro de que cuando Dan hizo Redux, solo nos estaba dando una herramienta en nuestro cinturón de herramientas como ayuda. No es el gato de todas las herramientas. No usa un martillo cuando puede atornillar el perno con un destornillador.

Dan incluso está de acuerdo .

Amo React y he estado trabajando en él durante casi dos años. Hasta ahora, no me arrepiento. La mejor decisión de todas. Me gusta Vue y todas las bibliotecas / marcos geniales que existen. Pero React ocupa un lugar especial en mi corazón. Me ayuda a concentrarme en el trabajo que se supone que debo hacer en lugar de dedicar todo mi tiempo a manipulaciones DOM. Y lo hace de la mejor y más eficaz forma posible. con su reconciliación efectiva.

He aprendido mucho en estos últimos años y he notado un problema común entre los desarrolladores de React nuevos y experimentados por igual: no usar React de la manera correcta cuando se trata de suscripciones o tareas asincrónicas. Siento que la documentación disponible no está bien redactada en este caso, así que decidí escribir este artículo.

Hablaré primero sobre las suscripciones y luego pasaremos a manejar la cancelación de tareas asincrónicas para evitar pérdidas de memoria en React (el objetivo principal de este artículo). Si no se maneja, esto ralentiza nuestra aplicación.

Ahora volvamos a ese hermoso mensaje de error del que hablamos inicialmente:

Advertencia: No se puede llamar a setState (o forceUpdate) en un componente desmontado. Esta es una operación no operativa, pero indica una pérdida de memoria en su aplicación. Para solucionarlo, cancele todas las suscripciones y tareas asincrónicas en el método componentWillUnmount.

Mi objetivo para este artículo es asegurarme de que nadie tenga que enfrentarse a este error sin saber qué hacer al respecto nuevamente.

Lo que cubriremos

  • Borrar suscripciones como setTimeout / setInterval
  • Borre las acciones asincrónicas cuando llame a una solicitud XHR usando fetchbibliotecas o comoaxios
  • Métodos alternativos, algunos obstinados, otros desaprobados.

Antes de comenzar, un gran saludo a Kent C Dodds , la persona más genial de Internet en este momento. Gracias por tomarse el tiempo y retribuir a la comunidad. Sus podcasts de YoutubeyLos cursos de cabeza de huevo sobre patrones de componentes de reacción avanzada son increíbles. Consulte estos recursos si desea dar el siguiente paso en sus habilidades de React.

Le pregunté a Kent sobre un mejor enfoque para evitar setState en el desmontaje del componente para poder optimizar mejor el rendimiento de React. Hizo todo lo posible e hizo un video al respecto. Si eres un tipo de persona con videos, échale un vistazo a continuación. Le dará un recorrido paso a paso con una explicación detallada.

Así que ahora vamos a empezar.

1: Borrar suscripciones

Comencemos con el ejemplo:

Hablemos de lo que acaba de pasar aquí. En lo que quiero que se concentre es en el counter.jsarchivo que básicamente incrementa el contador después de 3 segundos.

Esto da un error en 5 segundos, porque desmonté una suscripción sin borrarla. Si desea volver a ver el error, simplemente presione el botón de actualización en el editor CodeSandbox para ver el error en la consola.

Tengo mi archivo contenedor index.jsque simplemente cambia el componente contador después de los primeros cinco segundos.

Entonces

- - - → Index.js— - - - → Counter.js

En mi Index.js, llamo a Counter.js y simplemente hago esto en mi render:

{showCounter ?  : null}

El showCounteres un estado booleano que se establece en falso después de los primeros 5 segundos tan pronto como se monta el componente (componentDidMount).

Lo real que ilustra nuestro problema aquí es el counter.jsarchivo que incrementa la cuenta después de cada 3 segundos. Entonces, después de los primeros 3 segundos, el contador se actualiza. Pero tan pronto como llega a la segunda actualización, que ocurre en el sextosegundo, el index.jsarchivo ya ha desmontado el componente contador en el quintosegundo. Para cuando el componente del contador llega a su sextosegundo, actualiza el contador por segunda vez.

Actualiza su estado, pero aquí está el problema. No hay DOM para que el componente contador actualice el estado y es entonces cuando React arroja un error. Ese hermoso error que discutimos anteriormente:

Advertencia: No se puede llamar a setState (o forceUpdate) en un componente desmontado. Esta es una operación no operativa, pero indica una pérdida de memoria en su aplicación. Para solucionarlo, cancele todas las suscripciones y tareas asincrónicas en el método componentWillUnmount.

Ahora, si eres nuevo en React, podrías decir, “bueno Adeel… sí, pero ¿no desmontamos el componente Counter en el quinto segundo? Si no hay ningún componente para el contador, ¿cómo se puede actualizar su estado en el sexto segundo? "

Sí, tiene usted razón. Pero cuando hacemos algo como setTimeouto setIntervalen nuestros componentes React, no depende ni está vinculado con nuestra clase React como crees que puede ser. Seguirá funcionando después de su condición especificada a menos que cancele su suscripción o hasta que la cancele.

Ahora es posible que ya esté haciendo esto cuando se cumpla su condición. Pero, ¿qué pasa si su condición aún no se ha cumplido y el usuario decide cambiar las páginas en las que esta acción todavía está ocurriendo?

La mejor manera de eliminar este tipo de suscripciones es en su componentWillUnmountciclo de vida. A continuación, se muestra un ejemplo de cómo puede hacerlo. Consulte el método componentWillUnmount del archivo counter.js:

Y eso es todo para setTimout& setInterval.

2: abortos de API (XHR)

  • El viejo enfoque feo (obsoleto)
  • The Good Newer Approach (El objetivo principal de este artículo)

So, we’ve discussed subscriptions. But what if you make an asynchronous request? How do you cancel it?

The old way

Before I talk about that, I want to talk about a deprecated method in React called isMounted()

Before December 2015, there was a method called isMounted in React. You can read more about it in the React blog. What it did was something like this:

import React from 'react' import ReactDOM from 'react-dom' import axios from 'axios' class RandomUser extends React.Component { state = {user: null} _isMounted = false handleButtonClick = async () => { const response = await axios.get('//randomuser.me/api/') if (this._isMounted) { this.setState({ user: response.data }) } } componentDidMount() { this._isMounted = true } componentWillUnmount() { this._isMounted = false } render() { return ( Click Me 
{JSON.stringify(this.state.user, null, 2)}
) } }

For the purpose of this example, I am using a library called axios for making an XHR request.

Let’s go through it. I initially set this_isMounted to false right next to where I initialized my state. As soon as the life cycle componentDidMount gets called, I set this._isMounted to true. During that time, if an end user clicks the button, an XHR request is made. I am using randomuser.me. As soon as the promise gets resolved, I check if the component is still mounted with this_isMounted. If it’s true, I update my state, otherwise I ignore it.

The user might clicked on the button while the asynchronous call was being resolved. This would result in the user switching pages. So to avoid an unnecessary state update, we can simply handle it in our life cycle method componentWillUnmount. I simply set this._isMounted to false. So whenever the asynchronous API call gets resolved, it will check if this_isMounted is false and then it will not update the state.

Este enfoque hace el trabajo, pero como dicen los documentos de React:

El caso de uso principal isMounted()es evitar llamar setState()después de que un componente se haya desmontado, porque llamar setState()después de que un componente se haya desmontado emitirá una advertencia. La "advertencia setState" existe para ayudarlo a detectar errores, porque llamar setState()a un componente desmontado es una indicación de que su aplicación / componente de alguna manera no se limpió correctamente. Específicamente, llamar setState()a un componente desmontado significa que su aplicación aún tiene una referencia al componente después de que se ha desmontado, lo que a menudo indica una pérdida de memoria. Lee mas …

Esto significa que, aunque hemos evitado un setState innecesario, la memoria aún no se ha borrado. Todavía se está produciendo una acción asincrónica que no sabe que el ciclo de vida del componente ha finalizado y ya no es necesario.

Hablemos de la manera correcta

Aquí para salvar el día están AbortControllers . Según la documentación de MDN, dice:

La AbortControllerinterfaz representa un objeto controlador que le permite abortar una o más solicitudes DOM cuando lo desee. Lee mas ..

Veamos un poco más en profundidad aquí. Con código, por supuesto, porque todo el mundo ❤ código.

var myController = new AbortController(); var mySignal = myController.signal; var downloadBtn = document.querySelector('.download'); var abortBtn = document.querySelector('.abort'); downloadBtn.addEventListener('click', fetchVideo); abortBtn.addEventListener('click', function() { myController.abort(); console.log('Download aborted'); }); function fetchVideo() { ... fetch(url, { signal: mySignal }).then(function(response) { ... }).catch(function(e) { reports.textContent = 'Download error: ' + e.message; }) } 

First we create a new AbortController and assign it to a variable called myController. Then we make a signal for that AbortController. Think of the signal as an indicator to tell our XHR requests when it’s time to abort the request.

Assume that we have 2 buttons, Download and Abort . The download button downloads a video, but what if, while downloading, we want to cancel that download request? We simply need to call myController.abort(). Now this controller will abort all requests associated with it.

How, you might ask?

After we did var myController = new AbortController() we did this var mySignal = myController.signal . Now in my fetch request, where I tell it the URL and the payload, I just need to pass in mySignal to link/signal that FETCh request with my awesome AbortController.

Si desea leer un ejemplo aún más extenso AbortController, la gente genial de MDN tiene este ejemplo realmente agradable y elegante en su Github. Puede verificarlo aquí.

Quería hablar sobre estas solicitudes de aborto porque no muchas personas las conocen. La solicitud de un aborto en búsqueda comenzó en 2015. Aquí está el problema original de GitHub sobre el aborto: finalmente obtuvo soporte alrededor de octubre de 2017. Esa es una brecha de dos años. ¡Guauu! Hay algunas bibliotecas como axios que dan soporte a AbortController. Discutiré cómo puede usarlo con axios, pero primero quería mostrar la versión profunda y oculta de cómo funciona AbortController.

Anulación de una solicitud XHR en Axios

"Hacer o no hacer. Aquí no hay intentos." - Yoda

The implementation I talked about above isn’t specific to React, but that’s what we’ll discuss here. The main purpose of this article is to show you how to clear unnecessary DOM manipulations in React when an XHR request is made and the component is unmounted while the request is in pending state. Whew!

So without further ado, here we go.

import React, { Component } from 'react'; import axios from 'axios'; class Example extends Component { signal = axios.CancelToken.source(); state = { isLoading: false, user: {}, } componentDidMount() { this.onLoadUser(); } componentWillUnmount() { this.signal.cancel('Api is being canceled'); } onLoadUser = async () => { try { this.setState({ isLoading: true }); const response = await axios.get('//randomuser.me/api/', { cancelToken: this.signal.token, }) this.setState({ user: response.data, isLoading: true }); } catch (err) { if (axios.isCancel(err)) { console.log('Error: ', err.message); // => prints: Api is being canceled } else { this.setState({ isLoading: false }); } } } render() { return ( 
{JSON.stringify(this.state.user, null, 2)}
) } }

Let’s walk through this code

I set this.signal to axios.CancelToken.source()which basically instantiates a new AbortController and assigns the signal of that AbortController to this.signal. Next I call a method in componentDidMount called this.onLoadUser() which calls a random user information from a third party API randomuser.me. When I call that API, I also pass the signal to a property in axios called cancelToken

The next thing I do is in my componentWillUnmount where I call the abort method which is linked to that signal. Now let’s assume that as soon as the component was loaded, the API was called and the XHR request went in a pending state.

Now, the request was pending (that is, it wasn’t resolved or rejected but the user decided to go to another page. As soon as the life cycle method componentWillUnmount gets called up, we will abort our API request. As soon as the API get’s aborted/cancelled, the promise will get rejected and it will land in the catch block of that try/catch statement, particularly in the if (axios.isCancel(err) {} block.

Now we know explicitly that the API was aborted, because the component was unmounted and therefore logs an error. But we know that we no longer need to update that state since it is no longer required.

P.S: You can use the same signal and pass it as many XHR requests in your component as you like. When the component gets un mounted, all those XHR requests that are in a pending state will get cancelled when componentWillUnmount is called.

Final details

Congratulations! :) If you have read this far, you’ve just learned how to abort an XHR request on your own terms.

Let’s carry on just a little bit more. Normally, your XHR requests are in one file, and your main container component is in another (from which you call that API method). How do you pass that signal to another file and still get that XHR request cancelled?

Here is how you do it:

import React, { Component } from 'react'; import axios from 'axios'; // API import { onLoadUser } from './UserAPI'; class Example extends Component { signal = axios.CancelToken.source(); state = { isLoading: false, user: {}, } componentDidMount() { this.onLoadUser(); } componentWillUnmount() { this.signal.cancel('Api is being canceled'); } onLoadUser = async () => { try { this.setState({ isLoading: true }); const data = await onLoadUser(this.signal.token); this.setState({ user: data, isLoading: true }); } catch (error) { if (axios.isCancel(err)) { console.log('Error: ', err.message); // => prints: Api is being canceled } else { this.setState({ isLoading: false }); } } } render() { return ( 
{JSON.stringify(this.state.user, null, 2)}
) } }; }
export const onLoadUser = async myCancelToken => { try { const { data } = await axios.get('//randomuser.me/api/', { cancelToken: myCancelToken, }) return data; } catch (error) { throw error; } }; 

I hope this has helped you and I hope you’ve learned something. If you liked it, please give it some claps.

Gracias por tomarse el tiempo para leer. Le agradezco a mi talentoso colega Kinan por ayudarme a revisar este artículo. Gracias a Kent C Dodds por ser una inspiración en la comunidad JavaScript OSS.

Una vez más, me encantaría escuchar sus comentarios al respecto. Siempre puedes contactarme en Twitter .

También hay otra lectura asombrosa sobre Abort Controller que encontré a través de la documentación de MDN por Jake Archibald . Te sugiero que lo leas, si tienes una naturaleza curiosa como la mía.