Referenciar valores con refs

Cuando quieres que un componente “recuerde” alguna información, pero no quieres que esa información provoque 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

Añadir 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 argumento. Por ejemplo, esta es una ref al valor 0:

const ref = useRef(0);

useRef devuelve un objeto como este:

{
current: 0 // El valor que le pasaste a useRef
}
Una flecha con que tiene escrito 'current' metida en un bolsillo que tiene escrito 'ref'.

Illustrated by Rachel Lee Nabors

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 leerlo como modificarlo. Es como un bolsillo secreto de tu componente que React no puede rastrear. (Esto es lo que lo hace una “puerta 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 hace referencia a un número, pero, al igual que el estado, podrías hace referencia 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 rerenderiza con cada incremento. React (al igual que con el estado) preserva las refs entre rerenderizados. Sin embargo, asignar el estado rerenderiza un componente. ¡Cambiar una ref no!

Ejemplo: crear 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 se usa para el renderizado, así que guárdala 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 se presiona el botón “Detener”, necesitas cancelar el intervalo existente para que deje de actualizar la variable de estado now. Puedes hacerlo con una llamada a clearInterval, pero necesitas pasarle el identificador del intervalo que fue previamente devuelto por la llamada a setInterval cuando el usuario presionó Iniciar. Necesitas guardar el identificador del intervalo en alguna parte. Como el identificador de un intervalo no se usa para el renderizado, 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 el renderizado, guárdala en el estado. Cuando una pieza de información solo se necesita en los manejadores de eventos y no requiere un rerenderizado, 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 “estrictas” 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 “puerta de escape” que no necesitarás a menudo. Esta es la comparación entre el estado y las refs:

las refsel 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 rerenderizado cuando lo cambias.Desencadena un rerenderizado cuando lo cambias.
Mutable: puedes modificar y actualizar el valor de current fuera del proceso de renderizado.“Immutable”: necesitas usar la función asignadora del estado para modificar variables de estado para poner en cola un rerenderizado.
No deberías leer (o escribir) el valor de current durante el renderizado.Puedes leer el estado en cualquier momento. Sin embargo, cada renderizado tiene su propia instantánea del estado que 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. Cuando se asigna el valor del contador con setCount(), React rerenderiza el componente y la pantalla se actualiza para reflejar el nuevo contador.

Si intentaras implementarlo con una ref, React nunca rerenderizaría el componente, ¡y nunca verías cambiar el contador! 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 rerenderiza 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ódigo poco fiable. Si eso es lo que necesitas, usa en su lugar el estado.

Deep Dive

¿Cómo useRef funciona internamente?

A pesar de que React proporciona tanto useState como useRef, en principio useRef se podría implementar a partir de useState. Puedes imaginar que internamente en React, useRef se implementa 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 }. React almacena este objeto, así que durante el siguiente renderizado se devolverá el mismo objeto. Fíjate como el asignador de estado no se usa 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 imaginártelo 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 componente. Estas son algunas de estas situaciones raras:

Si tu componente necesita almacenar algún valor, pero no impacta la lógica del renderizado, 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 puerta de escape. Las refs son útiles cuando trabajas con sistemas externos o APIs del navegador. Si mucho de la lógica de tu aplicación y del flujo de los datos depende de las refs, es posible que quieras reconsiderar tu enfoque.
  • No leas o escribas ref.current durante el renderizado. Si se necesita alguna información durante el renderizado, usa en su lugar el estado. Como React no sabe cuándo ref.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 código como if (!ref.current) ref.current = new Thing() que 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 actualiza 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 se esté usando para el renderizado, 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 Manipular el DOM con refs.

Recapitulación

  • Las refs son una puerta de escape para guardar valores que no se usan en el renderizado. No las necesitarás a menudo.
  • Una ref es un objeto plano de JavaScript con una sola propiedad llamada current, que puedes leer o asignarle un valor.
  • Puedes pedirle a React que te dé una ref llamando al Hook useRef.
  • Como el estado, las refs retienen información entre los rerenderizados de un componente.
  • A diferencia del estado, asignar el valor de current de una ref no desencadena un rerenderizado.
  • No leas o escribas ref.current durante el renderizado. Esto hace que tu componente sea difícil 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 a clearTimeout para el identificador del timeout guardado durante handleSend. Sin embargo, incluso después de hacer clic en “Deshacer”, 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>
      }
    </>
  );
}