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 un ref.

Aprenderás

  • Cómo añadir un ref a tu componente
  • Cómo actualizar el valor de un ref
  • En qué se diferencian los refs y el estado
  • Cómo usar los refs de manera segura

Agregando un ref a tu componente

Puedes añadir un 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 un ref con el valor 0:

const ref = useRef(0);

useRef devuelve un objeto como este:

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

Puedes acceder al valor actual de ese 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>
  );
}

El 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, el 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, los refs son retenidos por React entre cada re-renderizado. Sin embargo, asignar el estado re-renderiza un componente. Cambiar un ref no!

Ejemplo: creando un cronómetro

Puedes combinar los 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 un 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 un ref quizás sea más eficiente.

Diferencias entre los refs y el estado

Tal vez estés pensando que los 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. Los refs son una “escotilla de escape” que no necesitarás a menudo. Esta es la comparación entre el estado y los refs:

los 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 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 un 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?

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 común sin un asignador. Si estas familiarizado con la programación orientada a objetos, los 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 un 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:

Si tu componente necesita almacenar algún valor, pero no impacta la lógica de la renderización, usa refs.

Buenas prácticas para los refs

Seguir estos principios hará que tus componentes sean más predecibles:

  • Trata a los refs como una escotilla de escape. Los 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 los 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á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 codigo como if (!ref.current) ref.current = new Thing() el cual solo asigna el ref una vez durante el renderizado inicial).

Las limitaciones del estado en React no se aplican a los 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 un ref, cambia inmediatamente:

ref.current = 5;
console.log(ref.current); // 5

Esto es porque el propio ref es un objeto regular de JavaScript, así que se comporta como uno.

Tampoco tienes que preocuparte por evitar la mutación cuando trabajas con un 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 el ref o con su contenido.

Los Refs y el DOM

Puedes apuntar un ref hacia cualquier valor. Sin embargo, el caso de uso más común para un ref es acceder a un elemento del DOM. Por ejemplo, esto es útil cuando quieres enfocar un input programáticamente. Cuando pasas un 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

  • Los refs son una escotilla de escape para quedarse con valores que no son usados para la renderización. No los necesitarás a menudo.
  • Un 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 un ref llamando al Hook useRef.
  • Como el estado, los refs retienen información entre los re-renderizados de un componente.
  • A diferencia del estado, asignar el valor de current de un 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>
      }
    </>
  );
}