React Hooks, useState y useEffect

React Hooks, useState y useEffect

驴Qu茅 son los React Hooks?

Los React Hooks, son funciones que se ejecutan en un determinado punto en la vida de un componente funcional. Permiten usar caracter铆sticas de React, sin la necesidad de usar clases. Por ejemplo te permite agregar state a un componente funcional.

Tambi茅n permite controlar efectos colaterales en caso de ser necesarios. Por ejemplo, peticiones a servicios, subscribirse a eventos, modificar el DOM, logging o cualquier otro c贸digo imperativo.

驴Por qu茅 funciones y componentes funcionales?

Si necesitas m谩s detalles sobre como son las funciones en Javascript, puedes revisar estas dos publicaciones:

  1. Funciones en Node.js y JavaScript. Lo realmente importante
  2. Funciones en Node.js y Javascript. M谩s detalles

Organizaci贸n y reutilizaci贸n

Se sabe que las funciones son la unida esencial para la organizaci贸n y reutilizaci贸n de c贸digo, es decir, las funcionalidades de cualquier software.

Al utilizar funciones se elimina la complejidad de usar clases. No necesitas usar this, constructores, ni separar funcionalidades estrechamente relacionadas en varios m茅todos del ciclo de vida de un componente en React.

Si has creado componentes en React, de seguro has aprendido ver a los componentes como una funci贸n y sus propiedades como sus par谩metros. 驴Por qu茅 no hacerlo m谩s transparentes al usar solamente funciones?

De hecho las clases, surgieron de funciones. Como se explica aqu铆 y aqu铆. No necesitamos clases y nos ahorramos muchos dolores de cabeza.

Composici贸n

Es mucho m谩s simple y flexible utilizar funciones en lugar de clases para la composici贸n, tanto de componentes como de cualquier funcionalidad en general.

Mejor composici贸n de objetos sobre herencia de clases

Design Patterns: Elements of Reusable Object-Oriented Software

Simplicidad

Normalmente con las clases, el flujo del c贸digo de los efectos colaterales necesita brincar de un m茅todo del ciclo de vida a otro, con React Hooks esto es m谩s lineal y f谩cil de leer. Tambi茅n la definici贸n del estado de un componente es mucho m谩s simple, sin necesidad de definirlo en el constructor.聽 En el siguiente punto viene un ejemplo de como los React Hooks son m谩s simples.

Se elimina el uso de componentes de alto nivel

Aunque los componentes de alto nivel (HOC) se basan en las funciones de alto nivel, la naturaleza del c贸digo HTML y clases hace que esto sea complejo. El c贸digo se vuelve dif铆cil de leer y tambi茅n provoca que los componentes envolventes se aniden demasiado.

Los React Hooks resuelven el famoso 鈥淗OC hell鈥, muy parecido a como las promesas y funciones as铆ncronas resuelven el 鈥淐allback hell鈥.

HOCs, complejo
HOCs, complejo

Ahora, si se utilizan React Hooks, esos HOC complejos se convierte en un c贸digo m谩s simple y lineal.

React hooks, simple y lineal
React hooks, simple y lineal.

M谩s r谩pido

Es un hecho que las funciones tienen mayor rendimiento que las clases. En el caso de React, un componente de funci贸n es m谩s r谩pido que un componente de alto nivel. Adem谩s, usando los React hooks adecuadamente, los componentes con React hooks suelen ser m谩s r谩pidos que los componentes de clases en React.

F谩cil de probar

Haciendo tus pruebas correctas evitando detalles de implementaci贸n, tu react hook deber铆a estar cubierto con tus pruebas del componente funcional. En otras publicaciones veremos c贸mo hacer esto. En caso de que tengas un React Hook complejo, existen herramientas que te facilitan estas pruebas aisladas o tambi茅n podemos hacer a mano nuestro wrapper que use nuestro React Hook para cada uno de sus casos.

Las dos reglas importantes de React Hooks

Solo ejecuta React Hooks en el nivel superior de la funci贸n

La clave de esta regla es que React depende del orden en que se ejecutan los React Hooks.

No ejecutes React Hooks dentro de condiciones, ciclos o funciones anidadas porque se necesita asegurar el orden correcto de los hooks cada vez que el componente se renderiza. Esto permite que React controle correctamente el estado entre multiples useState y useEffect.

Si existe alguna condici贸n en el c贸digo, y permite que a veces un hook se ejecute y otras no, el orden se pierde al igual que los datos correctos del estado. Hay que aclarar que las condiciones, ciclos y funciones anidadas dentro de los hooks como en el caso de useEffect si son posibles.

// No hagos esto!!
if (nombre) {
  useEffect(() => {
    document.title = `Algo con ${nombre}`;
  }, [nombre]);
}

// Esto si lo puedes hacer
useEffect(() => {
  if (nombre !== '') {
    document.title = `Algo con ${nombre}`;
  }
}, [nombre]);

Solo ejecuta React Hooks dentro de funciones de React

  1. Ejecuta React Hooks dentro de componentes de funci贸n.
    1. En las clases los React Hooks no funcionan, adem谩s en clases ya existen los m茅todos del ciclo de vida de un componente en React.
  2. Ejecuta React Hooks dentro de otros React Hooks (puedes hacer tus propios hooks).
    1. Para garantizar el orden y controlar correctamente el estado entre m煤ltiples Hooks. Te permite ejecutar react hooks al inicio de otro hook creado por ti.

Reac Hooks, useState(initialState)

Empecemos con el hook m谩s utilizado, useState. Este hook proporciona la misma capacidad de los componentes de clases para tener un estado interno. Vamos a hacer un formulario de registro de un usuario nuevo, algo sencillo y gen茅rico, nada de detalles de implementaci贸n espec铆ficos. Probablemente con el campo nombre ser谩 suficiente.

Vamos a utilizar la herramienta de codesandbox.io, usando la plantilla de React. Agregamos un archivo RegistroDeUsuario.js

import { useState } from "react";

export default function RegistroDeUsuario() {
  const [nombre, setNombre] = useState("");

  return (
    <form>
      <label htmlFor="nombre">Nombre</label>
      <input
        type="text"
        value={nombre}
        id="nombre"
        onChange={(e) => setNombre(e.target.value)}
      />
      <button type="submit">Enviar</button>

      <section>{nombre}</section>
    </form>
  );
}

Y en el archivo App.js importamos el componente RegistroDeUsuario

import RegistroDeUsuario from "./RegistroDeUsuario";

export default function App() {
  return (
    <div className="App">
      <RegistroDeUsuario />
    </div>
  );
}

Desctructuring assignment

Primero, la funci贸n useState(initialState), regresa un arreglo de dos elementos, el valor del estado (inicializado) que queremos controlar y un m茅todo para modificar ese estado. Este par de valores se asignaron a las constantes nombre y setNombre con la sintaxis de destructuring assignment.

El desctructuring assignment de la l铆nea 4 se traduce a lo siguiente.

const statePair = useState('');
const nombre = statePair[0];
const setNombre = statePair[1];

Invocaciones y renderizados

Cada vez que se escribe sobre el campo Nombre, se vuelve a renderizar el componente. Pero React guarda el estado entre renderizados. Se puede notar como la l铆nea 5 se ejecuta al primer r茅nder (Montar, con texto vac铆o) y tambi茅n cada vez que se actualiza el valor de nombre (Actualizar, revisa la consola de la parte derecha de la imagen de abajo).

El resultado debe ser algo como lo siguiente.

Renderizados por useState input Nombre
Renderizados por useState input Nombre

El flujo de useState es el siguiente. Modificado de este original.

Flujo useState

Flujo useState

El valor de nombre se actualiza a trav茅s del evento onChange y el m茅todo setNombre. Al modificar este estado interno, provoca la ejecuci贸n de la funci贸n RegistroDeUsuarios y un "re-renderizado". Si el componente se renderiza debido a un cambio a otro estado u otra propiedad, el estado de nombre permanece con la 煤ltima actualizaci贸n.

useState(() => { return initialState; })

useState(initialState) puede recibir una funci贸n que regrese el estado inicial usado en el primer render. Un ejemplo de su uso es el que sige, 驴Qu茅 podemos hacer si queremos guardar y obtener el estado de localStorage?

import { useState } from "react";

export default function RegistroDeUsuario() {
  const [nombre, setNombre] = useState(() => {
    console.log("Solo una vez");
    return localStorage.getItem("nombre") || "";
  });

  console.log("M谩s invocaciones");

  function actualizarNombre(e) {
    setNombre(e.target.value);
    localStorage.setItem("nombre", e.target.value);
  }

  return (
    <form>
      <label htmlFor="nombre">Nombre</label>
      <input
        type="text"
        value={nombre}
        id="nombre"
        onChange={actualizarNombre}
      />
      <button type="submit">Enviar</button>

      <section>{nombre}</section>
    </form>
  );
}

Ahora usamos una funci贸n para definir el estado, le agregamos un console.log('Solo una vez') para demostrar que la funci贸n solo se ejecuta una vez. Y un console.log('M谩s invocaciones') para demostrar que en los siguientes invocaciones ya no se ejecuta la funci贸n de nuestro useState(initialState), pero si el de M谩s invocaciones.

En el resultado de abajo, escrib铆 Jaime en el campo nombre, luego recargue la p谩gina, revisa el lado derecho en la vista y la consola.

campo nombre en localStorage
useState, campo nombre en localStorage

Al recargar se imprime Sola una vez y al empezar a escribir Cervantes se imprime M谩s invocaciones un total de 11 veces. Mi nombre jaime, lo obtuvo del localStorage al primer renderizado.

setState(prevState => {})

El m茅todo para actualizar el estado setNombre tambi茅n puede recibir una funci贸n, cuyo par谩metro es el valor anterior. Veamos un ejemplo modificando la funci贸n actualizarNombre.

function actualizarNombre(e) {
  setNombre(nombreAnterior => {
    console.log(nombreAnterior); // '', 'J', 'Ja', 'Jai', 'Jaim'
    return e.target.value;
  });
  localStorage.setItem("nombre", e.target.value);
}

La funci贸n setNombre obtenida de useState recibe como par谩metro el nombreAnterior, y al imprimir en la consola nos damos cuenta de que siempre imprimir谩 el valor anterior del estado nombre.

Actualizar state pasando una funci贸n
Actualizar el estado pasando una funci贸n

useEffect(effectFn, [deps])

Este React hook, useEffect, nos permite ejecutar y controlar efectos colaterales, como pueden ser peticiones a servicios, subscribirse a eventos, modificar el DOM o cualquier funcionalidad que no pueda ejecutarse en el cuerpo de nuestro componente funci贸n porque no pertenece al flujo lineal del mismo.

Flujo de useEffect

La funci贸n effectFn se ejecuta despu茅s de que el navegador ya ha pintado el componente en pantalla por primera vez (montar). Tambi茅n por defecto despu茅s de cada posterior repintado (actualizar). Este comportamiento descrito tiene el mismo prop贸sito que los m茅todos componentDidMount y componentDidUpdate.

El tercer prop贸sito en useEffect se le llama limpieza, el cual lo podemos comparar con componentDidUnmount. En el primer pintado (montar) la funci贸n de limpieza no se ejecuta, solo se ejecuta en la fase de actualizar. Es decir, se ejecuta despu茅s de cada repintando, pero antes del que el cuerpo de useEffect se ejecute. Este caso en espec铆fico se explica mejor con ejemplos en esta publicaci贸n.

El flujo de useEffect es el siguiente. Modificado de este original.

Flujo useEffect
Flujo useEffect.

useEffect recibe un segundo par谩metro, deps, el cual es un Array con la lista de dependencias que permiten decidir si ejecutar el efecto colateral o no, despu茅s de cada repintado. Sirve bastante para mejorar el rendimiento si no queremos que despu茅s de cada repintado se ejecute effectFn.

Si te das cuenta, he usado la palabra pintado en lugar de renderizado. Esto se debe a que efectivamente el efecto se ejecuta despu茅s de que los cambios ya est茅n pintados en el navegador web. El renderizado involucra al Virtual DOM, y React decide en que momento es conveniente actualizar el DOM, pero el pintado sucede un poco despu茅s de lo anterior. Aclaremos que aunque se actualice el DOM, el navegador debe calcular estilos y el layout de los elementos para posteriormente realizar el pintado de los pixeles.

useEffect(effectFn)

Veamos un ejemplo, aqu铆 la idea es tener un input de b煤squeda y queremos que lo que tengamos en el input se imprima en t铆tulo de la pesta帽a de nuestro navegador web. Para poder realizar esto necesitamos un efecto utilizando la API del DOM.

De nuevo, con la herramienta codesandbox.io y su plantilla de react, creamos un nuevo proyecto. Agregamos un archivo llamado OldNewsPapers.js donde vivir谩 nuestra funcionalidad en forma de un componente funcional.

import { useEffect, useState } from "react";

export default function OldNewsPapers() {
  const [query, setQuery] = useState("");

  useEffect(() => {
    console.log("document.title");
    document.title = `Periodicos viejos ${query}`;
  });
  
  console.log('Invocaci贸n);
  
  return (
    <>
      <h1>Peri贸dicos viejos que contienen {query}</h1>
      <form>
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
        />
      </form>
    </>
  );
}

Este efecto se ejecuta despu茅s de la primera vez que se pinta el componente, esto es el primer prop贸sito que se puede comparar con componentDidMount. 驴C贸mo es el flujo?

  1. Se ejecuta la funci贸n, es decir, el componente funcional.
    1. Lo cual inicializa el estado de query y el efecto colateral.
  2. Se renderiza y se pinta el elemento de react en el navegador con el valor inicial de query = ''.
    1. Texto 鈥淧eri贸dicos viejos que contienen鈥 en el <h1>.
  3. Se ejecuta el efecto colateral despu茅s del primer pintado.
    1. Texto 鈥淧eri贸dicos viejos que鈥 en el t铆tulo de la pesta帽a.
useEffect flujo al montar
useEffect flujo al montar.

Si escribimos "texas" en el input, ahora el flujo es de la siguiente manera

  1. Cada vez que se introduce una letra en el input (cinco veces m谩s, por las cinco letras de 鈥texas鈥)
    1. El estado de query cambia debido al setQuery en el onChange, provocando un nuevo pintado (invocaci贸n del componente funci贸n, renderizado y finalmente pintado).
    2. Despu茅s del pintado se actualiza document.title, cambiando el t铆tulo de la pesta帽a del navegador web.
useEffect, flujo actualizar document.title
useEffect, flujo actualizar document.title.

En la imagen de arriba vemos seis 鈥渄ocument.title鈥, como describimos al principio, por defecto el useEffect se invoca despu茅s de cada pintado en el navegador web.

Puedes ver el c贸digo completo en aqu铆.

useEffect(effectFn, [deps])

En el 煤ltimo ejemplo nuestro efecto se va a ejecutar despu茅s de cada pintado, incluso si el estado de query no ha cambiado. Para comprobar esto vamos a agregar otro estado para llevar la cuenta del n煤mero de invocaciones de nuestro componente funcional, que se traduce en el n煤mero de renderizados realizados. Estas invocaciones las haremos a trav茅s de un bot贸n.

import { useEffect, useState } from "react";

export default function OldNewsPapers() {
  const [query, setQuery] = useState("");
  const [invocations, setInvocations] = useState(1);

  useEffect(() => {
    console.log("document.title");
    document.title = `Peri贸dicos viejos ${query}`;
  });

  return (
    <>
      <h1>Peri贸dicos viejos que contienen {query}</h1>
      <p>{invocations} Invocaciones</p>
      <form>
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
        />
        <button
          onClick={(e) => {
            e.preventDefault();
            setInvocations((prev) => prev + 1);
          }}
        >
          Invocar
        </button>
      </form>
    </>
  );
}

Inicializamos invocations con 1, porque la primera vez que se renderiza ser谩 el estado actual. Luego si oprimimos el bot贸n Invocar, se cambia el valor de invocations, se hace otro re-renderizado, y luego se vuelve a ejecutar nuestro efecto, incluso cuando query no ha cambiado.

useEffect por defecto
useEffect por defecto.

useEffect por defecto siempre se ejecuta despu茅s de cada pintado

Para evitar que se ejecute demasiadas veces nuestro efecto, podemos indicarle que corra solo cuando una de sus dependencias ha cambiado. En este caso para evitar que se le asigne a cada rato el valor a documen.title, le indicamos que solo lo haga cuando query cambia.

import { useEffect, useState } from "react";

export default function OldNewsPapers() {
  const [query, setQuery] = useState("");
  const [invocations, setInvocations] = useState(1);

  useEffect(() => {
    console.log("document.title");
    document.title = `Peri贸dicos viejos ${query}`;
  }, [query]);

  return ( ... );
}

Ahora podemos ver que aunque hicimos muchas invocaciones con el bot贸n 鈥渋nvocar鈥, document.title solo se ejecut贸 la primera vez.

useEffect ejecutar solo cuando alguna "dep" cambie

useEffect se ejecuta solo cuando alguna "deps" cambie

useEffect(effectFn, [])

Cuando se especifica un Array vac铆o en las deps, effectFn solo se ejecuta una sola vez, despu茅s del primer pintado.

import { useEffect, useState } from "react";

export default function OldNewsPapers() {
  const [query, setQuery] = useState("");
  const [invocations, setInvocations] = useState(1);

  useEffect(() => {
    console.log("document.title");
    document.title = `Peri贸dicos viejos ${query}`;
  }, []);

  return ( ... );
}

Las siguientes veces que se actualice query escribiendo en el input, el t铆tulo de la pesta帽a ya no se actualiza.

useEffect con deps vac铆o
useEffect con deps vac铆o.

Conclusi贸n sobre

Las funciones han existido desde mucho antes de la programaci贸n, gracias al c谩lculo lambda de Alonzo Church. No es extra帽o que en los 煤ltimos a帽os el desarrollo de software ha volteado hacia la programaci贸n funcional debido a la simplicidad y el poder expresivo. Resolviendo varios problemas en el camino.

Y bueno, con el uso de React Hooks, se ha dado un paso muy importante debido a los beneficios que es programar de esta manera, desde hace a帽os que se utilizaban componentes funcionales, y ahora con esto creo que React tiene m谩s futuro prometedor por delante.

Hemos entendido, con suficiente profundidad (para comenzar con hooks), como funcionan los flujos de los Hooks useState y useEffect. De useEffect a煤n quedan temas por ver, as铆 como tambi茅n los React Hooks personalizados. Aqu铆 pondr茅 el enlace con los temas pendientes, cuando estos est茅n publicados. Cualquier duda, no dudes en escribirla en los comentarios, 隆Estaremos contentos de ayudarte!.

Referencias

https://reactjs.org/docs/hooks-intro.html

https://reactjs.org/docs/hooks-state.html

https://reactjs.org/docs/hooks-effect.html

Foto de fondo de portada por Artem Sapegin en Unsplash

Podr铆a interesarte

驴C贸mo crear un servidor web en 5 minutos con Node.js?

Para crear un servidor web en node.js, primero, 驴Qu茅 es node.js?, tomando la definici贸n del sitio oficial, es un entorno de ejecuci贸n para JavaScript construido con el motor de JavaScript V8 de Chrome. Node.js usa un modelo de operaciones E/S sin bloqueo y orientado a eventos, que lo hace liviano y eficiente. El ecosistema de paquetes de Node.js, npm, es el ecosistema m谩s grande de librer铆as de c贸digo abierto en el mundo.