React Hooks Personalizados con useEffect

React Hooks Personalizados con useEffect

Ya se explicó como usar useEffect en esta publicación, sin embargo aún queda una variante muy importante que de hecho es la variante más compleja, por esta razón merece ser parte de este contenido.

Inicialmente crearemos un ejemplo con esta forma de useEffect, la cual tendrá una función de retorno que sirve para hacer limpieza, llamaremos limpieza a esta función a lo largo de todo el contenido. Adicionalmente, crearemos dos React hooks personalizados para extraer la funcionalidad de la variante limpieza en piezas más pequeñas.

Como recordatorio, useEffect nos permite ejecutar y controlar efectos colaterales, como pueden ser peticiones a servicios, subscribirse a eventos, modificar el DOM o cualquier funcionalidad de manera imperativa.

Recordando el flujo de useEffect(effectFn, [deps])

Primero, la función effecFn (el primer parámetro que recibe useEffect) se ejecuta después de que el navegador ya ha pintado el componente en pantalla por primera vez (montar). Segundo, la función effectFn se ejecuta después de cada posterior repintado (actualizar). Estos dos comportamientos descritos tienen 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. Esto se debe a que la función limpieza, en realidad no existe aún en el primer pintado porque es el valor de retorno de effectFn.

El flujo de useEffect se representa en la imagen de abajo. Modificado de este original.

Flujo useEffect
Flujo useEffect

Haciendo una petición a un servicio con useEffect(effectFn, [deps])

En un proyecto nuevo de codesandbox.io, crea el archivo OldNewsPapers.js. Lo que se busca con este componente es hacer peticiones a un servicio, donde podemos buscar información sobre periódicos antiguos de USA.

En resumen, la funcionalidad de este ejemplo es que en el campo de búsqueda el usuario escriba algún texto relacionado, cuando el usuario se detenga de escribir por un tiempo mayor al definido, entonces es cuando se dispara la petición.

Primero vamos a hacer solo la petición sin preocuparnos del tiempo definido de espera para ejectuarla.

import { useEffect, useState } from "react";

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

  useEffect(() => {
    if (query) {
      console.log("fetch -> ", query);
      fetchOldNewsPapers();
    }

    async function fetchOldNewsPapers() {
      const response = await fetch(
        `https://chroniclingamerica.loc.gov/search/pages/results/?andtext=${query}&format=json`
      );
      const json = await response.json();
      setNewsPaperNumber(json.totalItems);
    }
  }, [query]);

  return (
    <>
      <h1>Periódicos viejos que contienen {query}</h1>
      <form>
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
        />
      </form>
      <section>{newsPaperNumber}</section>
    </>
  );
}

Aquí está el ejemplo en codesandbox.io. Cada vez que query es modificado, se realiza una petición, como se ve en la imagen de abajo.

useEffect usando fetch
useEffect usando fetch

Al instante que el usuario empieza a escribir “texas”, se actualiza el estado de query provocando un “repintado” y se ejecuta la función effecFn por cada letra que se inserte.

Desafortunadamente hacer peticiones a un servicio cada vez que escribimos una letra no es un buen funcionamiento, es mejor solo hacer la petición cuando el usuario deja de escribir, tal como se planteó al principio, asi que vamos a implementar esta mejor solución en la siguiente sección.

useEffect(() => { return function limpieza(){}; }, [deps])

Comentamos previamente que esta función de limpieza se ejecuta solamente en los “repintados”, en el primer pintado no se ejecuta porque la función limpieza es el valor de retorno de la función effectFn. En el pintado número uno la función limpieza aún no existe.

Ejemplo

Para hacer la petición al servicio cuando el usuario deje de escribir, vamos a utilizar un temporizador, este temporizador estará creándose y limpiándose después de cada repintado. Cuando el usuario se detenga de escribir y pase el tiempo definido en el temporizador, ahí es donde haremos la petición al servicio.

import { useEffect, useState } from "react";

export default function OldNewsPapers() {
  const [query, setQuery] = useState("");
  const [newsPaperCount, setNewsPaperCount] = useState(0);
  const [isTyping, setIsTyping] = useState(false);

  useEffect(() => {
    console.log("función effectFn");
    if (query && !isTyping) {
      console.log("  fetch-->", query);
      fetchOldNewsPaperTitles();
    }

    async function fetchOldNewsPaperTitles() {
      const response = await fetch(
        `https://chroniclingamerica.loc.gov/search/pages/results/?andtext=${query}&format=json`
      );
      const json = await response.json();
      setNewsPaperCount(json.totalItems);
    }

    let timeoutId;
    if (isTyping) {
      timeoutId = setTimeout(() => {
        console.log('  setIsTyping(false), provoca otro "re-pintado"');
        setIsTyping(false);
      }, 1000);
      console.log("  timeout", timeoutId);
    }

    return () => {
      console.log("----------re-pintado--------------");
      console.log("función limpieza");
      if (timeoutId) {
        clearTimeout(timeoutId);
        console.log("  clearTimeout", timeoutId);
      }
    };
  }, [query, isTyping]);

  return (
    <>
      <h1>Periódicos viejos que contienen {query}</h1>
      <form>
        <input
          type="text"
          value={query}
          onChange={(e) => {
            setIsTyping(true);
            setQuery(e.target.value);
            console.log(
              `onChange: setIsTyping(true) y setQuery(${e.target.value})`
            );
          }}
        />
      </form>
      <section>{newsPaperCount}</section>
    </>
  );
}

Al código de arriba le dejé varios console.log para que podamos ver el flujo, si quieres se los puedes eliminar para mayor claridad. Aquí esta este ejemplo completo en codesandbox.io.

useEffect con limpieza
useEffect con limpieza

Explicación de flujo

Después del primer “pintado” se ejecuta effectFn, luego cuando el usuario empieza a escribir “texas” provoca un “repintado”, lo anterior sucede por cada letra insertada debido al evento onChange que actualiza el estado de query.

Posterior a cada repintado se ejecuta primero la función limpieza, eliminando el temporizador, luego la función effectFn volviendo a activar un temporizador y retornando una nueva función limpieza.

Cuando el usuario deja de escribir, la función callback del setTimeout se ejecuta, provocando la actualización del estado isTyping a false. Lo anterior causa otro repintado adicional, teniendo a isTyping igual a false y query igual a “texas”, siendo las condiciones (query && !isTyping) necesarias para realizar una petición. En el momento que el resultado de la petición está listo, se actualiza setNewsPaperCount con el número de periódicos antiguos que contiene la palabra “texas”. La actualización de setNewsPaperCount provoca un último repintado, solo que en este último repintado no se ejecuta la función limpieza ni effectFn porque query y isTyping, las dependencias de nuestro useEffect, permanecen sin cambiar.

React Hooks personalizados

Las funciones son la unida esencial para la organización y reutilización de código, ya sea solo para separar las responsabilidades de cada función y/o para encapsular código/funcionalidad reutilizable. Los React Hooks perzonalizados, al ser funciones también tienen esta habilidad de organización y reutilización.

Vamos a extraer funciones de nuestro ejemplo anterior con la finalidad de hacer más fácil de leer el código, lo primero que podemos ver es que dentro del useEffect, tenemos un código que realiza una petición usando el API de fetch y otro para llevar el control del temporizador. Tal vez valga la pena separarlos, a estas funciones separadas se les llama React hooks personalizados porque la separación se realiza en forma de Hooks

React Hook personalizado useContinueTimeout(trigger, waitTime)

Empezamos con el código del temporizador. Aquí está el código extraído, bueno, con algunas modificaciones para poder ser reutilizado.

function useCanContinueTimeout(trigger, waitTime) {
  const [canContinue, setCanContinue] = useState(false);

  useEffect(() => {
    let timeoutId;
    if (canContinue) {
      timeoutId = setTimeout(() => {
        setCanContinue(false);
      }, waitTime);
    }
  
    return () => {
      clearTimeout(timeoutId);
    }
  }, [trigger, canContinue]);

  return [canContinue, setCanContinue];
}

Lo que hacemos en realidad es pasar tal cual el código que queremos separar, y luego cambiar los nombres de los identificadores para mejorar su lectura y entendimiento.

Explicación del Reack hook personalizado useCanContinueTimeout

Este React Hook personalizado, puede ser utilizando en cualquier campo de búsqueda donde se quiera realizar una acción después de que el usuario deja de escribir por un tiempo determinado. Recibe dos parámetros, un valor que es el trigger o disparador principal del efecto. Y otro valor que indica el tiempo del temporizador (waitTime), si no se limpia y se crea otro temporizador, canContinue es actualizado a false indicando que la acción de escribir dejo de suceder durante el tiempo indicado por waitTime.

El estado interno canContinue condiciona si puede seguir creando temporizadores o no, si es false ya no puede crear temporizadores, porque la acción de escribir dejo de suceder y si es true indica que el usuario está escribiendo. Se expone este estado canContinue al exterior regresando un par de valores tal cual lo hace useState para su manejo.

La razón por la que se expone al exterior canContinue es porque es más flexible que desde donde se use este hook se pueda definir inmediatamente en que momento puede seguir creando temporizadores o no.

En el caso particular del ejemplo solo le interesa indicarle que si puede continuar creando temporizadores mientras el usuario siga escribiendo dentro del campo de búsqueda.

¿Cómo se puede usar el react hook personalizado useCanContinueTimeout?

Este Hooks se puede usar de la siguiente forma con el ejemplo que hemos estado trabajando.

const WAIT_TIME = 1000;

export default function OldNewsPapers() {
  const [query, setQuery] = useState('');
  const [isTyping, setIsTyping] = useCanContinueTimeout(query, WAIT_TIME);
  ...
  return (...);
}

Aunque se creó pensando en el uso del campo de búsqueda, queda claro que pude ser usado para saber si se detuvo o continua cualquier otra acción. Puede usarse en cargadores, en notificaciones temporales, acciones de scroll en el navegador web y de momento no se me ocurre otro caso, es una primera versión del hook, pero es un ejemplo de como podemos extraer funcionalidad con react hooks personalizados.

React Hook personalizado useGetJsonWhenCondition(url, condition)

Ahora es el turno de la función que realiza la petición a un servicio, primero separamos la funcionalidad, al igual que con useCanContinueTimeout solo copiamos el código y cambiamos los nombres de las dependencias de useEffect.

function useFetchOldNewsPaper(query, condition) {
  const [newsPaperCount, setNewsPaperCount] = useState(0);

  useEffect(() => {
    if (query && !condition) {
      doFetch()
    }
    async function doFetch() {
      const response = await fetch(
        `https://chroniclingamerica.loc.gov/search/pages/results/?andtext=${query}&format=json`
      );
      const json = await response.json();
      setNewsPaperCount(json.totalItems);
    }
  }, [query, condition]);

  return newsPaperCount;
}

React hook personalizado useGetJsonWhenCondition, más reutilizable

Ahora vamos a refactorizarlo para hacerlo más reutilizable.

function useGetJsonWhenCondition(url, condition) {
  const [state, setState] = useState({});

  useEffect(() => {
    if (url && condition) {
      (async () => {
        const response = await fetch(url);
        const json = await response.json();
        setState(json);
      })();
    }
  }, [url, condition]);

  return state;
}

De entrada le cambiamos el nombre para que tenga más sentido sobre lo que hace realmente, obtener un json cuando una condición se cumple. Luego ya no obtendremos solo el número de resultados, mejor ahora guardamos la respuesta completa. Además para no crear una función separada abajo de la condición, se utilizó una función autoinvocada (function() {})().

Solo se hará la petición si se tiene una url y la condición se cumple, el manejo de estos dos datos no es responsabilidad de este hook, así que se la delegamos al usuario final, que en nuestro caso es el componente OldNewsPapers.

Funcion async/await del useEffect

Creamos una función anónima para poder hacer la petición asincrónicamente sin hacer a effectFn asíncrona, ¿Por qué no queremos hacer la función effectFn asíncrona? La respuesta en realidad es simple, normalmente la función effectFn regresa undefined o una función normal de limpieza, pero si la hacemos asíncrona deberá regresar una promesa, lo cual romperá con el funcionamiento normal de valores de retorno de useEffect. Por no mencionar que función effectFn es invocada de manera normal, no de forma async/await.

Aclaramos que la forma anterior donde primero se definía la función por separado de la invocación dentro del effectFn es equivalente a esta función anónima autoinvocada. Al final regresamos el estado del hook, que es la respuesta del servicio o un objeto vacío cuando la respuesta aún no se ha obtenido.

Extraer en su propia función a fetch con petición GET para respuestas JSON.

Finalmente vamos a extraer la funcionalidad de hacer la peticion para que sea mas simple de leer el efecto

function useGetJsonWhenCondition(url, condition) {
  const [state, setState] = useState({});

  useEffect(() => {
    if (url && condition) {
      (async () => {
        setState(await getJson(url));
      })();
    }
  }, [url, condition]);

  return state;
}

// Es simple , sin ontrol de errores para no perder el enfoque de los hooks
async function getJson(url) {
  const response = await fetch(url)
  return await response.json();
}

Separamos el uso de fetch en su propia función getJson, porque el uso de fetch y la llamada al servicio involucra un manejo más detallado de errores, de momento lo dejamos de manera simple separando la invocación.

Implementamos el react hook personalizado useGetJsonWhenCondition en nuestro componente OldNewsPapers

Ahora si se puede utilizar el hook en nuestro componente de ejemplo, de la siguiente forma.

export default function OldNewsPapers({ url }) {
  const [query, setQuery] = useState('');
  const [fetchUrl, setFetchUrl] = useState('');
  const [isTyping, setIsTyping] = useCanContinueTimeout(query, WAIT_TIME);
  const newsPapers = useGetJsonWhenCondition(fetchUrl, !isTyping);

  // Dejamos el manejo de url y la actualización de canCantinue al componente que los utiliza
  useEffect(() => {
    if (query) {
      setIsTyping(true);
      setFetchUrl(url.replace('{query}', query));  
    } else {
      setFetchUrl('');
    }
  }, [query, url]);

  return (
    <>
      <h1>Periodicos viejos que contienen {query}</h1>
      <form>
        <input
          type="text"
          value={query}
          onChange={(e) => {
            setQuery(e.target.value);
          }}
        />
      </form>
      <section>
        {newsPapers.totalItems}
      </section>
    </>
  );
}

Como vemos el manejo de la condición y la formación de la url se la dejamos el componente que usa el hook, no al hook useGetJsonWhenCondition(url, condition). De esta manera el React Hook personalizado useGetJsonWhenCondition(url, condition) puede ser reutilizado, es decir, el componente OldNewsPapers depende de los dos React Hooks personalizados que acabamos de crear, no al revés.

Aquí puedes revisar el ejemplo completo.

Puntos importantes al usar y crear React Hooks Personalizados

Invocaciones y renderizados

Los componentes funcionales se vuelven a ejecutar cuando algo en el estado cambia, y por consiguiente también se ejecutan las funciones de react hooks personalizados. En los ejemplos que vimos, cada vez que cambia algo en el estado de query, fetchUrl, isTyping y newsPapers, provocara otra invocación del componente función OldNewsPaper, por consiguiente también se invocaran las funciones useCanContinueTimeout y useGetJsonWhenCondition (React hooks personalizados) que creamos y todo esto se traduce a un re-renderizado.

¿Por qué NO queremos hacer la función effectFn asíncrona?

La respuesta en realidad es simple, normalmente la función effectFn regresa undefined o una función normal de limpieza, pero si la hacemos asíncrona, deberá regresar una promesa, lo cual romperá con el funcionamiento normal de valores de retornode useEffect. Por no mencionar que función effectFn es invocada de manera normal, no de forma async/await.

Reglas de React hooks personalizados

Las mismas dos reglas importantes aplicadas a los React Hooks, son tambien aplicadas a los React Hooks personalizados.

  1. Solo ejecuta React Hooks al incio de una función componente o React Hook personalizado
  2. Solo ejecuta React Hooks dentro de funciones de React

Los nombres de los React hooks personalizados deben empezar con “use"

Esto es una convención importante, de esta manera cualquier desarrollador a simple vista se da cuenta que se trata de un React Hook. Ademas asi se nombran los Hooks de React y asi el plugin de eslint detecta posibles errores en el codigo relacionados con las reglas de los hooks.

Cada React hook personalizado, tiene su propio estado

Cada invocación de un React Hook personalizado contiene su propio estado, los Hooks no tienen estados compartidos, porque recordemos que useState() guarda un estado totalmente separado de cualquier otra invocación. Además cada invocación de una función contiene su propio ámbito.

Conclusiones

En esta publicación aprendimos como usar la variante más compleja de useEffect(() => { return function limpieza(){}; }, [deps]) y además se extrajo dos React Hooks personalizados porque el useEffect inicial es complejo. Se estableció que la creación de React Hooks personalizados es igual que cuando extraemos funciones más pequeñas de una función grande. Por último vimos los puntos a tomar en cuenta cuando creamos y usamos React Hooks personalizados.

Referencias

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

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

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

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

Deja un comentario

A %d blogueros les gusta esto: