Banner keyboard

Andrés D'Amelio

Desarrollador web Front-end

Título

Ingeniero en informática

Ubicación

Venezuela

React Hooks: Guía useRef

Publicado el 21 de abril de 2022

En publicaciones pasadas conocimos los hooks useState y useEffect, cómo funcionan y cómo podemos utilizarlos en nuestros proyectos. En esta ocasión conoceremos el hook useRef y cómo este hook nos ofrece características interesantes.


En la documentación de React podemos encontrar la siguiente definición “useRef devuelve un objeto ref mutable cuya propiedad .current se inicializa con el argumento pasado (initialValue). El objeto devuelto se mantendrá persistente durante la vida completa del componente.”


De ese concepto podemos tomar varias cosas que nos permiten conocer cómo se usa el hook. En primer lugar tenemos que recibe un argumento, este argumento no es obligatorio, y su valor será el que tendrá la propiedad current del objeto creado. El objeto persiste durante todo el ciclo de vida de nuestro componente. Este hook tiene dos usos principales: ser un medio de acceso al DOM y mantener una variable mutable. Más adelante hablaré sobre cada caso.


Importación


Este hook, lo podemos obtener desde react de la siguiente forma:


import { useRef } from 'react';

Como podemos ver, su importación es similar al resto de hooks que hemos visto en artículos anteriores, ahora veamos cómo crear una referencia:


const refContainer = useRef(initialValue);

Con esto tenemos una referencia, que podemos utilizar según sea necesario, cuando usamos este hook lo que se crea es un objeto con la siguiente estructura:


{
    current: initialValue
}

Si no pasamos el initialValue, se le asignará por defecto undefined.


Una característica relevante que tiene el useRef es que persiste entre renderizados. Esto debido a que el objeto creado existe fuera del ciclo de renderizado del componente, haciendo que su valor persista durante todo el ciclo de vida. Es importante resaltar qué, cuando cambia la propiedad current del objeto, éste no genera un nuevo renderizado.


Acceso al DOM


Con useRef podemos asignar una referencia a un elemento del DOM, esto nos permite acceder a un elemento específico de forma rápida. Si estuviésemos trabajando con Vanilla, podemos obtener un elemento del DOM utilizando los métodos de acceso, como getElementById, de esta forma:


document.getElementById('id');

Con esto tenemos acceso a ese nodo del DOM y podemos modificar sus atributos, agregar eventos, etc. De igual forma ocurre con useRef, creamos una referencia, se la agregamos a un elemento y luego podemos accederlo a través de la propiedad current. Veamos un ejemplo típico donde tenemos un input, y queremos que al cargar la página el foco este sobre ese elemento:


import { useEffect, useRef } from 'react';

const MyComponent = () => {
  const refElement = useRef();

  useEffect(() => {
    refElement.current.focus();
  }, []);

  return (
    <form>
      <label htmlFor='email'>Nombre</label>
      <input
        type='email'
        ref={refElement}
        id='email'
        placeholder='ingresa tu correo'
      />

      <label htmlFor='password'>Contraseña</label>
      <input type='password' id='password' placeholder='*******' />

      <button>Entrar</button>
    </form>
  );
};

export default MyComponent;

En este caso, tenemos un formulario, y queremos que al renderizarse completamente, el input de correo tenga el foco, es por esto que en nuestro useEffect activamos el foco en el input. Durante el proceso de renderizado inicialmente .current toma el valor de undefined, al crear la referencia aún no se ha creado una estructura del DOM, por lo que aún nuestra referencia no esta asociada a ningún elemento. Luego tener una estructura, .current cambia su valor.


Nota : Alterar una referencia se considera un efecto secundario, por lo que debemos agregar la lógica que modifica la referencia dentro del hook useEffect para evitar comportamientos inesperados.


Otro caso que podemos encontrar es el reenvío de referencia entre componentes, es decir, supongamos que tenemos un componente padre, donde creamos una referencia y se la queremos pasar a un componente hijo y asignarla a uno de sus nodos. Podríamos pensar en pasarlo al componente y obtenerlo desde props de nuestro componente, de la siguiente manera:


import { useEffect, useRef } from "react";

// Componente hijo
const Input = ({ ref, type, placeholder, id }) => {
  return <input 
      type={type} 
      ref={ref}
      id={id} 
      placeholder={placeholder} 
    />
};

const MyComponent = () => {
  const refElement = useRef();

  useEffect(() => {
    refElement.current.focus();
  }, []);

  return (
    <form>
      <label htmlFor="email">Nombre</label>
      <Input
        type="email"
        ref={refElement}
        id="email"
        placeholder="ingresa tu correo"
      />

      <label htmlFor="password">Contraseña</label>
      <Input 
        type="password" 
        id="password" 
        placeholder="*******" 
      />

      <button>Entrar</button>

    </form>
  );
};

export default MyComponent;

Este caso no funcionaria, porque los componentes de función no recibe ref como argumento, ni mucho menos están disponibles como propiedad en el objeto props. Para solucionar esto, utilizamos la función forwardRef disponible en react, que recibe un callback, y en los argumentos de ese callback, tenemos los props y la referencia. Veamos como quedaría nuestro ejemplo:


import React, { useEffect, useRef } from "react";

const Input = React.forwardRef(( {type, placeholder, id }, ref) => {
  return <input 
      type={type} 
      ref={ref}
      id={id} 
      placeholder={placeholder} 
    />
});

const MyComponent = () => {
  const refElement = useRef();

  useEffect(() => {
    refElement.current.focus();
  }, []);

  return (
    <form>
      <label htmlFor="email">Nombre</label>
      <Input
        type="email"
        ref={refElement}
        id="email"
        placeholder="ingresa tu correo"
      />

      <label htmlFor="password">Contraseña</label>
      <Input 
        type="password" 
        id="password" 
        placeholder="*******" 
      />

      <button>Entrar</button>

    </form>
  );
};

export default MyComponent;

Y así ya podemos pasar la referencia a los componentes hijos, y hacer todas las modificaciones al nodo asociado desde el componente padre.


Mantener una variable mutable


Como mencione al inicio del artículo podemos utilizar useRef de diferentes maneras, una de estas es para almacenar información mutable. Esto lo podemos hacer con el hook useState pero la principal diferencia que tiene useRef es que no dispara un nuevo renderizado cuando su propiedad current cambia.


Un ejemplo de uso de useRef es capturar el estado anterior de una variable, supongamos que tenemos una variable de estado que almacena un valor, si queremos guardar el estado anterior de esa variable podemos usar useRef, y como no se ve afectada en cada renderizado podemos mantener el valor durante el ciclo de vida. Veamos un ejemplo:


import { useRef, useState } from "react";

const MyComponent = () => {
  const [number, setNumber] = useState(0);
  const refValue = useRef(0);

  const change = () => {
    const value = Math.random().toFixed(2);
    refValue.current = number;
    setNumber(value);
  };
  const prevValue = refValue.current;

  return (
    <div>
      <p>
        Valores: Nuevo {number}, Anterior {prevValue}{" "}
      </p>
      <button onClick={change}>Cambiar valor</button>
    </div>
  );
};

export default MyComponent;

Como se puede ver, tenemos una variable de estado, que cambia cada vez que pulsamos un botón, al pulsar el botón le asignamos un valor random, y adicional cambiamos el valor de la propiedad current, con el anterior valor que tiene la variable antes de cambiar.


Con esto, luego de cada renderizado que se hace al cambiar el estado, podemos obtener la variable con el valor anterior, y mostrarla en pantalla.


Como desarrolladores de React, en ocasiones nos hemos encontrado con el siguiente error: “Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.”


En concreto, lo que nos dice el error es que no podemos cambiar el estado de un componente que no está montado, es decir, si estamos usando variables de estado debemos asegurarnos de que este componente este disponible para poder cambiar el estado. Veamos un ejemplo de un caso donde nos podemos encontrar con este error.


import { useEffect, useState } from 'react';

const Cocktail = () => {
  const [state, setState] = useState({
    loading: true,
    data: null
  });

  useEffect(() => {
    fetch('https://www.thecocktaildb.com/api/json/v1/1/random.php')
      .then((response) => response.json())
      .then((result) => {
        setState({
          loading: false,
          data: result.drinks[0]
        });
      });
  }, []);

  return !state.loading ? (
    <div>
      <h1>Nombre: {state.data.strDrink}</h1>
      <p>Preparación: {state.data.strInstructions}</p>
    </div>
  ) : (
    <p>Cargando cocktail</p>
  );
};

const App = () => {
  const [show, setShow] = useState(true);

  return (
    <div>
      {show && <Cocktail />}
      <button onClick={() => setShow(!show)}>
        {show ? 'Ocultar' : 'Mostrar'}
      </button>
    </div>
  );
};

export default App;

En este ejemplo, tenemos un componente que realiza una petición a una API para obtener un Cocktail, luego de obtenerlo se actualiza el estado. Este componente es llamado condicionalmente, si se pulsa varias veces el botón del componente principal montará y desmontará el componente Cocktail en múltiples ocasiones. Cuando el componente se ha desmontado el useEffect sigue ejecutándose, por lo que intentará actualizar el estado, y obtendremos el error antes mencionado.


Para evitar este error vamos a usar useRef y nos ayudaremos de la función de limpieza del useEffect, que no permitirá mutar el valor de la referencia y así poder controlar la actualización del estado.


import { useEffect, useRef, useState } from 'react';

const Cocktail = () => {
  const isMounted = useRef(true);
  const [state, setState] = useState({
    loading: true,
    data: null
  });

  useEffect(() => {
    fetch('https://www.thecocktaildb.com/api/json/v1/1/random.php')
      .then((response) => response.json())
      .then((result) => {
        if (isMounted.current) {
          setState({
            loading: false,
            data: result.drinks[0]
          });
        }
      });

    return () => {
      isMounted.current = false;
    };
  }, []);

  return !state.loading ? (
    <div>
      <h1>Nombre: {state.data.strDrink}</h1>
      <p>Preparación: {state.data.strInstructions}</p>
    </div>
  ) : (
    <p>Cargando cocktail</p>
  );
};

const App = () => {
  const [show, setShow] = useState(1);

  return (
    <div>
      {show && <Cocktail />}
      <button onClick={() => setShow(!show)}>
        {show ? 'Ocultar' : 'Mostrar'}
      </button>
    </div>
  );
};

export default App;

Como podemos ver, tenemos creamos una nueva referencia isMounted cuyo valor inicial es true, esta referencia la usaremos para determinar si el componente esta o no montado. En nuestro useEffect agregamos la función de limpieza, para modificar nuestra referencia. En la llamada a la API verificamos si el componente esta montado, así si ha sido desmontado no se realiza la actualización del estado y evitamos obtener el error antes mencionado.


Conclusión


Pudimos ver que el hook useRef es una característica muy importante cuando necesitamos acceder a elementos del DOM o mantener una variable mutable. Sin embargo, es recomendable no abusar de su uso. Conocimos todos los posibles casos de uso, y como este hook nos puede ayudar.


Si te gusto el artículo, compártelo con tus amigos o en tus redes para llegar a más personas. Si tienes alguna duda o comentario no dudes en dejarme un comentario y te responderé en la brevedad posible.


¿Que otro hook te gustaría conocer?