Manejo de accesorios y estados obsoletos en los componentes funcionales de React

Índice
  1. Accesorios y estados obsoletos
  2. La forma sucia de almacenar accesorios y estados en una referencia.
  3. La forma más limpia de almacenar accesorios y estados en una referencia.
  4. Usando un gancho personalizado
  5. Hacer que esto funcione con accesorios
  6. Una última cosa…

Hay un aspecto de JavaScript que siempre me tiene tirando de los pelos: los cierres . Trabajo mucho con React y la superposición allí es que a veces pueden ser la causa de accesorios y estados obsoletos . Veremos exactamente lo que eso significa, pero el problema es que los datos que utilizamos para crear nuestra interfaz de usuario pueden ser totalmente incorrectos de maneras inesperadas, lo cual, ya sabes, es malo.

Accesorios y estados obsoletos

En pocas palabras: es cuando el código que se ejecuta de forma asincrónica tiene una referencia a un accesorio o estado que ya no es nuevo y, por lo tanto, el valor que devuelve no es el más reciente.

Para ser aún más claro, juguemos con el mismo ejemplo de referencia obsoleto que React tiene en su documentación.

function Counter() {  const [count, setCount] = useState(0);  function handleAlertClick() {    setTimeout(() = {      alert("You clicked on: " + count);    }, 3000);  }  return (    div      pYou clicked {count} times/p      button onClick={() = setCount(count + 1)}Click me/button      button onClick={handleAlertClick}Show alert/button    /div  );}

( Demo en vivo )

Nada especial aquí. Tenemos un componente funcional llamado Counter. Realiza un seguimiento de cuántas veces el usuario ha hecho clic en un botón y muestra una alerta que muestra cuántas veces se hizo clic en ese botón al hacer clic en otro botón. Prueba esto:

  1. Haga clic en el botón “Haga clic en mí”. Verás cómo aumenta el contador de clics.
  2. Ahora haga clic en el botón “Mostrar alerta”. Deben pasar tres segundos y luego activar una alerta que le indicará cuántas veces hizo clic en el botón “Haga clic en mí”.
  3. Ahora, haga clic en el botón “Mostrar alerta” nuevamente y haga clic rápidamente en el botón “Haga clic en mí” antes de que active la alerta en tres segundos.

¿Mira qué pasa? El recuento que se muestra en la página y el recuento que se muestra en la alerta no coinciden. Sin embargo, el número en la alerta no es simplemente un número aleatorio. Ese número es el valor que tenía la variable en el momento en que se definió countla función asincrónica dentro de ella , que es el momento en que se hace clic en el botón “Mostrar alerta”.setTimeout

Así es como funcionan los cierres. No vamos a entrar en detalles sobre ellos en esta publicación, pero aquí hay algunos documentos que los cubren con mayor detalle.

Centrémonos en cómo podemos evitar estas referencias obsoletas con nuestros estados y accesorios.

React ofrece un consejo sobre cómo lidiar con fechas y accesorios obsoletos en la misma documentación donde se extrajo el ejemplo.

Si intencionalmente desea leer el estado más reciente de alguna devolución de llamada asincrónica, puede guardarlo en un archivoref , modificarlo y leerlo.

Al mantener el valor de forma asincrónica en a ref, podemos evitar las referencias obsoletas. Si necesita saber más sobre reflos componentes funcionales, la documentación de React tiene mucha más información .

Entonces, eso plantea la pregunta: ¿Cómo podemos mantener nuestros accesorios o estado en un ref?

Primero hagámoslo de la manera sucia.

La forma sucia de almacenar accesorios y estados en una referencia.

Podemos crear fácilmente una referencia usando useRef()y usar countcomo su valor inicial . Luego, dondequiera que se actualice el estado, configuramos la ref.currentpropiedad con el nuevo valor. Por último, utilícelo ref.currenten lugar de counten la parte asincrónica de nuestro código.

function Counter() {  const [count, setCount] = useState(0);  const ref = useRef(count); // Make a ref and give it the count  function handleAlertClick() {    setTimeout(() = {      alert("You clicked on: " + ref.current); // Use ref instead of count    }, 3000);  }  return (    div      pYou clicked {count} times/p      button        onClick={() = {          setCount(count + 1);          ref.current = count + 1; // Update ref whenever the count changes        }}              Click me      /button      button        onClick={() = {          handleAlertClick();        }}              Show alert      /button    /div  );}   

( Demo en vivo )

Continúe y haga lo mismo que la última vez. Haga clic en “Mostrar alerta” y luego haga clic en “Hacer clic en mí” antes de que se active la alerta en tres segundos.

¡Ahora tenemos el último valor!

He aquí por qué funciona. Cuando la función de devolución de llamada asincrónica se define dentro setTimeout, guarda una referencia a las variables que utiliza, que es counten este caso. De esta manera, cuando el estado se actualiza, React no solo cambia el valor sino que la referencia de la variable en la memoria también es completamente diferente.

Esto significa que, incluso si el valor del estado no es primitivo, la variable con la que estás trabajando en tu devolución de llamada asincrónica no es la misma en la memoria. Un objeto que normalmente mantendría su referencia en diferentes funciones ahora tiene un valor diferente.

¿Cómo se resuelve esto usando un ref? Si volvemos a echar un vistazo rápido a los documentos de React, encontramos información interesante, pero fácil de pasar por alto:

[…] useRefte dará el mismo refobjeto en cada renderizado.

No importa lo que hagamos. A lo largo de la vida útil de su componente, React nos proporcionará exactamente el mismo objeto de referencia en la memoria. Cualquier devolución de llamada, sin importar cuándo se define o ejecuta, funciona con el mismo objeto. No más referencias obsoletas.

La forma más limpia de almacenar accesorios y estados en una referencia.

Seamos honestos… usar algo refasí es una solución fea. ¿Qué pasa si tu estado se actualiza en mil lugares diferentes? Ahora tienes que cambiar tu código y actualizarlo manualmente refen todos esos lugares. Eso es un no-no.

Haremos esto más escalable dando refel valor del estado automáticamente cuando el estado cambie.

Comencemos por deshacernos del cambio manual en refel botón “Haz clic en mí”.

A continuación, creamos una función llamada updateStateque se llama cada vez que necesitamos cambiar el estado. Esta función toma el nuevo estado como argumento, establece la ref.currentpropiedad en el nuevo estado y también actualiza el estado con ese mismo valor.

Finalmente, sustituyamos la setCountfunción original que nos proporciona React por la nueva updateStatefunción donde se actualiza el estado.

function Counter() {  const [count, setCount] = useState(0);  const ref = useRef(count);  // Keeps the state and ref equal  function updateState(newState) {    ref.current = newState;    setCount(newState);  }  function handleAlertClick() { ... }  return (    div      pYou clicked {count} times/p      button        onClick={() = {          // Use the created function instead of the manual update          updateState(count + 1);        }}              Click me      /button      button onClick={handleAlertClick}Show alert/button    /div  );}       

( Demo en vivo )

Usando un gancho personalizado

La solución limpiadora funciona bien. Hace el trabajo igual que la solución sucia, pero solo llama a una función para actualizar el estado y ref.

¿Pero adivina que? Podemos hacerlo mejor. ¿Qué pasa si necesitamos agregar más estados? ¿Qué pasa si queremos hacer esto también en otros componentes? Tomemos el estado refy updateStatefuncionemos y hagámoslos verdaderamente portátiles. ¡Ganchos personalizados al rescate!

Fuera del Countercomponente, vamos a definir una nueva función. Pongámosle un nombre useAsyncReference. (En realidad, puede tener cualquier nombre, pero tenga en cuenta que es una práctica común nombrar ganchos personalizados con “uso” como prefijo). Nuestro nuevo gancho tendrá un solo parámetro por ahora. Lo llamaremos value.

Nuestra solución anterior tenía la misma información almacenada dos veces: una en el estado y otra en el ref. Vamos a optimizar eso manteniendo el valor solo en refeste tiempo. En otras palabras, crearemos a refy le daremos el valueparámetro como valor inicial.

Justo después de ref, crearemos una updateStatefunción que tome el nuevo estado y lo establezca en la ref.currentpropiedad.

Por último, devolvemos una matriz con refy la updateStatefunción, muy similar a lo que hace React con useState.

function useAsyncReference(value) {  const ref = useRef(value);  function updateState(newState) {    ref.current = newState;  }  return [ref, updateState];}function Counter() { ... }

¡Nos estamos olvidando de algo! Si revisamos lauseRef documentación, aprendemos que la actualización de a refno activa una nueva representación. Entonces, mientras reftenga el valor actualizado, no veremos los cambios en la pantalla. Necesitamos forzar una nueva renderización cada vez que refse actualice.

Lo que necesitamos es un Estado falso. El valor no importa. Sólo estará ahí para provocar la repetición. Incluso podemos ignorar el estado y mantener solo su función de actualización. Llamamos a esa función de actualización forceRendery le damos un valor inicial de false.

Ahora, dentro de updateState, forzamos la repetición llamándolo forceRendery pasándole un estado diferente al actual después de configurarlo ref.currenten newState.

function useAsyncReference(value) {  const ref = useRef(value);  const [, forceRender] = useState(false);  function updateState(newState) {    ref.current = newState;    forceRender(s = !s);  }  return [ref, updateState];}function Counter() { ... }  

Toma cualquier valor que tenga y devuelve lo contrario. El estado realmente no importa. Simplemente lo estamos cambiando para que React detecte un cambio de estado y vuelva a renderizar el componente.

A continuación, podemos limpiar el Countcomponente y eliminar la función y la utilizada anteriormente useState, luego implementar el nuevo gancho. El primer valor de la matriz devuelta es el estado en forma de . Seguiremos llamándolo recuento, donde el segundo valor es la función para actualizar el estado/ . Seguiremos llamándolo .refupdateStaterefrefsetCount

También tenemos que cambiar las referencias al conteo ya que ahora deben ser todas count.current. Y debemos llamar setCounten lugar de llamar updateState.

function useAsyncReference(value) { ... }function Counter() {  const [count, setCount] = useAsyncReference(0);  function handleAlertClick() {    setTimeout(() = {      alert("You clicked on: " + count.current);    }, 3000);  }  return (    div      pYou clicked {count.current} times/p      button        onClick={() = {          setCount(count.current + 1);        }}              Click me      /button      button onClick={handleAlertClick}Show alert/button    /div  );}    

Hacer que esto funcione con accesorios

Tenemos una solución verdaderamente portátil para nuestro problema. Pero adivina qué… todavía queda un poco más por hacer. Específicamente, necesitamos hacer que la solución sea compatible con los accesorios.

Tomemos el botón “Mostrar alerta” y handleAlertClickfuncionemos con un nuevo componente fuera del Countercomponente. Lo llamaremos Alerty se necesitará un único accesorio llamado count. Este nuevo componente mostrará el countvalor de propiedad que le estamos pasando en una alerta después de un retraso de tres segundos.

function useAsyncReference(value) { ... }function Alert({ count }) {  function handleAlertClick() {    setTimeout(() = {      alert("You clicked on: " + count);    }, 3000);  }  return button onClick={handleAlertClick}Show alert/button;}function Counter() { ... }

En Counter, estamos intercambiando el botón “Mostrar alerta” por el Alertcomponente. Pasaremos count.currental countutilería.

function useAsyncReference(value) { ... }function Alert({ count }) { ... }function Counter() {  const [count, setCount] = useAsyncReference(0);  return (    div      pYou clicked {count.current} times/p      button        onClick={() = {          setCount(count.current + 1);        }}              Click me      /button      Alert count={count.current} /    /div  );} 

( Demo en vivo )

Muy bien, es hora de volver a realizar los pasos de prueba. ¿Ver? Aunque estamos usando una referencia segura al conteo en Counter, la referencia al countaccesorio en el Alertcomponente no es asincrónicamente segura y nuestro gancho personalizado no es adecuado para usar con accesorios… todavía.

Por suerte para nosotros, la solución es bastante sencilla.

Todo lo que tenemos que hacer es agregar un segundo parámetro a nuestro useAsyncReferencegancho llamado isProp, con falseel valor inicial. Justo antes de devolver la matriz con refy updateState, configuramos una condición. Si isPropes así true, configuramos la ref.currentpropiedad en valuey solo regresamos ref.

function useAsyncReference(value, isProp = false) {  const ref = useRef(value);  const [, forceRender] = useState(false);  function updateState(newState) {    ref.current = newState;    forceRender(s = !s);  }  if (isProp) {    ref.current = value;    return ref;  }  return [ref, updateState];}function Alert({ count }) { ... }function Counter() { ... }     

Ahora actualicemos Alertpara que use el gancho. Recuerde pasar truecomo segundo argumento ya useAsyncReferenceque estamos pasando un accesorio y no un estado.

function useAsyncReference(value) { ... }function Alert({ count }) {  const asyncCount = useAsyncReference(count, true);  function handleAlertClick() {    setTimeout(() = {      alert("You clicked on: " + asyncCount.current);    }, 3000);  }  return button onClick={handleAlertClick}Show alert/button;}function Counter() { ... }  

( Demo en vivo )

Inténtalo de nuevo. Ahora funciona perfectamente ya sea que uses estados o accesorios.

Una última cosa…

Hay un último cambio que me gustaría hacer. useState Los documentos de React nos dicen que React saldrá de una nueva renderización si el nuevo estado es idéntico al anterior. Nuestra solución no hace eso. Si volvemos a pasar el estado actual a la updateStatefunción del gancho, forzaremos una nueva representación pase lo que pase. Cambiemos eso.

Pongamos el cuerpo updateStatedentro de una declaración if y ejecútelo cuando ref.currentsea diferente al nuevo estado. La comparación debe hacerse con Object.is(), tal como lo hace React.

function useAsyncReference(value, isProp = false) {  const ref = useRef(value);  const [, forceRender] = useState(false);  function updateState(newState) {    if (!Object.is(ref.current, newState)) {      ref.current = newState;      forceRender(s = !s);    }  }  if (isProp) {    ref.current = value;    return ref;  }  return [ref, updateState];}function Alert({ count }) { ... }function Counter() { ... }    

¡Por fin hemos terminado!


React a veces puede parecer una caja negra llena de pequeñas peculiaridades. Puede resultar desalentador lidiar con esas peculiaridades, como la que acabamos de abordar. Pero si es paciente y disfruta de los desafíos, pronto se dará cuenta de que es un marco increíble y que es un placer trabajar con él.

SUSCRÍBETE A NUESTRO BOLETÍN 
No te pierdas de nuestro contenido ni de ninguna de nuestras guías para que puedas avanzar en los juegos que más te gustan.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir

Este sitio web utiliza cookies para mejorar tu experiencia mientras navegas por él. Este sitio web utiliza cookies para mejorar tu experiencia de usuario. Al continuar navegando, aceptas su uso. Mas informacion