Manejo de accesorios y estados obsoletos en los componentes funcionales de React
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:
- Haga clic en el botón “Haga clic en mí”. Verás cómo aumenta el contador de clics.
- 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í”.
- 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ó count
la 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 archivo
ref
, 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 ref
los 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 count
como su valor inicial . Luego, dondequiera que se actualice el estado, configuramos la ref.current
propiedad con el nuevo valor. Por último, utilícelo ref.current
en lugar de count
en 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 count
en 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:
[…]
useRef
te dará el mismoref
objeto 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 ref
así 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 ref
en todos esos lugares. Eso es un no-no.
Haremos esto más escalable dando ref
el valor del estado automáticamente cuando el estado cambie.
Comencemos por deshacernos del cambio manual en ref
el botón “Haz clic en mí”.
A continuación, creamos una función llamada updateState
que se llama cada vez que necesitamos cambiar el estado. Esta función toma el nuevo estado como argumento, establece la ref.current
propiedad en el nuevo estado y también actualiza el estado con ese mismo valor.
Finalmente, sustituyamos la setCount
función original que nos proporciona React por la nueva updateState
funció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 ref
y updateState
funcionemos y hagámoslos verdaderamente portátiles. ¡Ganchos personalizados al rescate!
Fuera del Counter
componente, 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 ref
este tiempo. En otras palabras, crearemos a ref
y le daremos el value
parámetro como valor inicial.
Justo después de ref
, crearemos una updateState
función que tome el nuevo estado y lo establezca en la ref.current
propiedad.
Por último, devolvemos una matriz con ref
y la updateState
funció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 ref
no activa una nueva representación. Entonces, mientras ref
tenga el valor actualizado, no veremos los cambios en la pantalla. Necesitamos forzar una nueva renderización cada vez que ref
se 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 forceRender
y le damos un valor inicial de false
.
Ahora, dentro de updateState
, forzamos la repetición llamándolo forceRender
y pasándole un estado diferente al actual después de configurarlo ref.current
en 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 Count
componente 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 .ref
updateState
ref
ref
setCount
También tenemos que cambiar las referencias al conteo ya que ahora deben ser todas count.current
. Y debemos llamar setCount
en 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 handleAlertClick
funcionemos con un nuevo componente fuera del Counter
componente. Lo llamaremos Alert
y se necesitará un único accesorio llamado count
. Este nuevo componente mostrará el count
valor 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 Alert
componente. Pasaremos count.current
al count
utilerí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 count
accesorio en el Alert
componente 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 useAsyncReference
gancho llamado isProp
, con false
el valor inicial. Justo antes de devolver la matriz con ref
y updateState
, configuramos una condición. Si isProp
es así true
, configuramos la ref.current
propiedad en value
y 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 Alert
para que use el gancho. Recuerde pasar true
como segundo argumento ya useAsyncReference
que 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 updateState
función del gancho, forzaremos una nueva representación pase lo que pase. Cambiemos eso.
Pongamos el cuerpo updateState
dentro de una declaración if y ejecútelo cuando ref.current
sea 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.
Deja un comentario