Haciendo referencia a valores utilizando Refs
Cuando quieres que un componente “recuerdo” alguna información, pero no quieres que esa información active nuevos renderizados, puedes usar una ref.
Aprenderás
- Cómo añadir una ref a tu componente
- Cómo actualizar el valor de una ref
- En qué se diferencian las refs y el estado
- Cómo usar las refs de manera segura
Agregando una ref a tu componente
Puedes añadir una ref a tu componente importando el Hook useRef
desde React:
import { useRef } from 'react';
Dentro de tu componente, llama al Hook useRef
y pasa el valor inicial al que quieres hacer referencia como único parámetro. Por ejemplo, este es una ref con el valor 0
:
const ref = useRef(0);
useRef
devuelve un objeto como este:
{
current: 0 // El valor que le pasaste al useRef
}
Puedes acceder al valor actual de esa ref a través de la propiedad ref.current
. Este valor es mutable intencionalmente, lo que significa que puedes tanto leer como escribir en él. Es como un bolsillo secreto de tu componente que React no puede rastrear. (Esto es lo que lo hace una “escotilla de escape” del flujo de datos de una vía de React—más sobre eso a continuación!)
Aquí, un botón incrementará ref.current
en cada clic:
import { useRef } from 'react'; export default function Counter() { let ref = useRef(0); function handleClick() { ref.current = ref.current + 1; alert('Has hecho clic ' + ref.current + ' veces!'); } return ( <button onClick={handleClick}> Clic aquí! </button> ); }
La ref apunta hacia un número, pero, como el estado, podrías apuntar a cualquier cosa: un string, un objeto, o incluso una función. A diferencia del estado, la ref es un objeto plano de JavaScript con la propiedad current
que puedes leer y modificar.
Fíjate como el componente no se re-renderiza con cada incremento. Como el estado, las refs son retenidos por React entre cada re-renderizado. Sin embargo, asignar el estado re-renderiza un componente. Cambiar una ref no!
Ejemplo: creando un cronómetro
Puedes combinar las refs y el estado en un solo componente. Por ejemplo, hagamos un cronómetro que el usuario pueda iniciar y detener al presionar un botón. Para poder mostrar cuánto tiempo ha pasado desde que el usuario pulsó “Iniciar”, necesitarás mantener rastreado cuándo el botón de Iniciar fue presionado y cuál es el tiempo actual. Esta información es usada para la renderización, asi que la guardala en el estado:
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
Cuando el usuario presione “Iniciar”, usarás setInterval
para poder actualizar el tiempo cada 10 milisegundos:
import { useState } from 'react'; export default function Stopwatch() { const [startTime, setStartTime] = useState(null); const [now, setNow] = useState(null); function handleStart() { // Empieza a contar. setStartTime(Date.now()); setNow(Date.now()); setInterval(() => { // Actualiza el tiempo actual cada 10 milisegundos. setNow(Date.now()); }, 10); } let secondsPassed = 0; if (startTime != null && now != null) { secondsPassed = (now - startTime) / 1000; } return ( <> <h1>Tiempo transcurrido: {secondsPassed.toFixed(3)}</h1> <button onClick={handleStart}> Iniciar </button> </> ); }
Cuando el boton “Detener” es presionado, necesitas cancelar el intervalo existente para que deje de actualizar la variable now
del estado. Puedes hacer esto llamando clearInterval
, pero necesitas pasarle el identificador del intervalo que fue previamente devuelto por la llamada del setInterval
cuando el usuario presionó Iniciar. Necesitas guardar el identificador del intervalo en alguna parte. Como el identificador de un intervalo no es usado para la renderización, puedes guardarlo en una ref:
import { useState, useRef } from 'react'; export default function Stopwatch() { const [startTime, setStartTime] = useState(null); const [now, setNow] = useState(null); const intervalRef = useRef(null); function handleStart() { setStartTime(Date.now()); setNow(Date.now()); clearInterval(intervalRef.current); intervalRef.current = setInterval(() => { setNow(Date.now()); }, 10); } function handleStop() { clearInterval(intervalRef.current); } let secondsPassed = 0; if (startTime != null && now != null) { secondsPassed = (now - startTime) / 1000; } return ( <> <h1>Tiempo transcurrido: {secondsPassed.toFixed(3)}</h1> <button onClick={handleStart}> Iniciar </button> <button onClick={handleStop}> Detener </button> </> ); }
Cuando una pieza de información es usada para la renderización, guárdala en el estado. Cuando una pieza de información solo se necesita en los manejadores de eventos y no requiere un re-renderizado, usar una ref quizás sea más eficiente.
Diferencias entre las refs y el estado
Tal vez estés pensando que las refs parecen menos “estrictos” que el estado—puedes mutarlos en lugar de siempre tener que utilizar una función asignadora del estado, por ejemplo. Pero en la mayoría de los casos, querrás usar el estado. Las refs son una “escotilla de escape” que no necesitarás a menudo. Esta es la comparación entre el estado y las refs:
las refs | el estado |
---|---|
useRef(initialValue) devuelve { current: initialValue } | useState(initialValue) devuelve el valor actual de una variable de estado y una función asignadora del estado ( [value, setValue] ) |
No desencadena un re-renderizado cuando lo cambias. | Desencadena un re-renderizado cuando lo cambias. |
Mutable—puedes modificar y actualizar el valor de current fuera del proceso de renderización. | “Immutable”—necesitas usar la función asignadora del estado para modificar variables de estado para poner en cola un re-renderizado. |
No deberías leer (o escribir) el valor de current durante la renderización. | Puedes leer el estado en cualquier momento. Sin embargo, cada renderizado tiene su propia instantánea del estado la cual no cambia. |
Este es un botón contador que está implementado con el estado:
import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); } return ( <button onClick={handleClick}> Has hecho {count} clics </button> ); }
Como el valor de count
es mostrado, tiene sentido usar un valor del estado para eso. Cuando el valor del contador es asignado con setCount()
, React re-renderiza el componente y la pantalla se actualiza para reflejar la nueva cuenta.
Si trataste de implementar esto con una ref, React nunca re-renderizaría el componente, así que nunca verías la cuenta cambiar! Observa como al hacer clic en este botón no se actualiza su texto:
import { useRef } from 'react'; export default function Counter() { let countRef = useRef(0); function handleClick() { // Esto no re-renderiza el componente! countRef.current = countRef.current + 1; } return ( <button onClick={handleClick}> Has hecho {countRef.current} clics </button> ); }
Esta es la razón por la que leer ref.current
durante el renderizado conduce a un cógigo poco fiable. Si necesitas eso, en su lugar usa el estado.
Deep Dive
¿Cómo useRef funciona internamente?
¿Cómo useRef funciona internamente?
A pesar de que tanto useState
como useRef
son proporcionados por React, en principio useRef
podría ser implementado por encima de useState
. Puedes imaginar que internamente en React, useRef
es implementado de esta manera:
// Internamente en React
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}
Durante el primer renderizado, useRef
devuelve { current: initialValue }
. Este objeto es almacenado por Reacto, asi que durante el siguiente renderizado el mismo objeto será devuelto. Fíjate como el asignador de estado no es usado en este ejemplo. Es innecesario porque useRef
siempre necesita devolver el mismo objeto!
React proporciona una versión integrada de useRef
porque es suficientemente común en la practica. Pero puedes pensar en ello como si fuera una variable de estado normal sin un asignador. Si estas familiarizado con la programación orientada a objetos, las refs puede que te recuerden a los campos de instancias—pero en lugar de this.something
escribes somethingRef.current
.
Cuándo usar refs
Típicamente, usarás una ref cuando tu componente necesite “salir” de React y comunicarse con APIs externas—a menudo una API del navegador no impactará en la apariencia de un componentete. Estas son algunas de estas raras situaciones:
- Almacenar identificadores de timeouts
- Almacenar y manipular Elementos del DOM, lo cual cubrimos en la siguiente página
- Almacenar otros objetos que no son necesarios para calcular el JSX.
Si tu componente necesita almacenar algún valor, pero no impacta la lógica de la renderización, usa refs.
Buenas prácticas para las refs
Seguir estos principios hará que tus componentes sean más predecibles:
- Trata a las refs como una escotilla de escape. Las refs son útiles cuando trabajas con sistemas externos o APIs del navegador. Si mucha de la lógica de tu aplicación y el flujo de los datos dependen de las refs, puede que quieras reconsiderar su enfoque.
- No leas o escribas
ref.current
durante la renderización. Si se necesita alguna información durante la renderización, en su lugar usa el estado. Como React no sabe cuándoref.current
cambia, incluso leerlo mientras se renderiza hace que el comportamiento de tu componente sea difícil de predecir. (La única excepción a esto es codigo comoif (!ref.current) ref.current = new Thing()
el cual solo asigna la ref una vez durante el renderizado inicial).
Las limitaciones del estado en React no se aplican a las refs. Por ejemplo, el estado actúa como una instantánea para cada renderizado y no se actualíza de manera síncrona. Pero cuando mutas el valor actual de una ref, cambia inmediatamente:
ref.current = 5;
console.log(ref.current); // 5
Esto es porque la propia ref es un objeto normal de JavaScript, así que se comporta como uno.
Tampoco tienes que preocuparte por evitar la mutación cuando trabajas con una ref. Siempre y cuando el objeto que estás mutando no está siendo usado para la renderización, a React no le importa lo que hagas con la ref o con su contenido.
Las Refs y el DOM
Puedes apuntar una ref hacia cualquier valor. Sin embargo, el caso de uso más común para una ref es acceder a un elemento del DOM. Por ejemplo, esto es útil cuando quieres enfocar un input programáticamente. Cuando pasas una ref a un atributo ref
en JSX, así <div ref={myRef}>
, React colocará el elemento del DOM correspondiente en myRef.current
. Puedes leer más sobre esto en Manipulando el DOM con refs.
Recapitulación
- Las refs son una escotilla de escape para quedarse con valores que no son usados para la renderización. No los necesitarás a menudo.
- Una ref es un objeto plano de JavaScript con una sola propiedad llamada
current
, la cual puedes leer o asignarle un valor. - Puedes pedirle a React que te de una ref llamando al Hook
useRef
. - Como el estado, las refs retienen información entre los re-renderizados de un componente.
- A diferencia del estado, asignar el valor de
current
de una ref no desencadena un re-renderizado. - No leas o escribas
ref.current
durante la renderización. Esto hace que tu componente sea díficil de predecir.
Desafío 1 de 4: Arregla un input de chat roto
Escribe un mensaje y haz clic en “Enviar”. Notarás que hay un retraso de tres segundos antes de que veas la alerta de “Enviado!“. Durante este retraso, puedes ver un botón de “Deshacer”. Haz clic en él. Este botón de “Deshacer” se supone que debe evitar que el mensaje de “Enviado!” aparezca. Hace esto llamando clearTimeout
para el identificador del timeout guardado durante handleSend
. Sin embargo, incluso después de que “Deshacer” es clicado, el mensaje de “Enviado!” sigue apareciendo. Encuentra por qué no funciona, y arréglalo.
import { useState } from 'react'; export default function Chat() { const [text, setText] = useState(''); const [isSending, setIsSending] = useState(false); let timeoutID = null; function handleSend() { setIsSending(true); timeoutID = setTimeout(() => { alert('Enviado!'); setIsSending(false); }, 3000); } function handleUndo() { setIsSending(false); clearTimeout(timeoutID); } return ( <> <input disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <button disabled={isSending} onClick={handleSend}> {isSending ? 'Enviando...' : 'Enviar'} </button> {isSending && <button onClick={handleUndo}> Deshacer </button> } </> ); }