7 min read
Introduciendo React Query

Buenas a todos!

Os quiero hablar de un descubrimiento de hace unos meses pero que hasta ahora no había podido poner en práctica, y que para mí ha sido un antes y un después a la hora de manejar estado en una aplicación en React: React-Query.

logo

Gracias a una charla de su autor, Tanner Linsley, en el React Summit, me decidí tras un tiempo a echarle un ojo, porque me pareció super interesante, y quería compartir con vosotros un poco mis impresiones y los problemas que me ha solucionado.

TL;DR

  • React Query reduce la complejidad de manejar estados asíncronos.
  • Podrías pensar que es capaz de “conectar” dichos estados y las queries entre sí.
  • Distingue entre queries y mutaciones (acciones que cambian datos en BBDD y hacen las queries obsoletas).
  • Se acabó el gestionar los casos de loadings y error.

Una intro sobre React Query

Siempre me ha parecido que lo que veía sobre gestionar estado en el front no terminaban de resolver los problemas de la asincronía, y requerían de muchos trozos de código iguales, que aunque se podían refactorizar, siempre acababas centrándote en controlar estos estados, y no en lo que de verdad tiene importancia.

Pero qué es React Query?

Pues no es más que una libraría para manejar estado de Backend. React Query puede manejar las peticiones a una API por ti, y puede gestionar cuándo debes actualizar los datos, incluso de forma automática.

Queries y mutaciones

La forma en la que React Query es capaz de identificar cada petición es mediante una key que le indicaremos. Además, distingue entre queries y mutaciones. Las queries las podrá hacer de forma automática, y podrá manejar su estado “fresh” (sus datos están actualizados) o “stale” (sus datos están obsoletos). React Query te proporciona unos hooks para manejarlas, useQuery y algunos más, dependiendo del tipo de query que queramos hacer (podemos hacer paginadas, infinitas…).

Sin embargo, las mutaciones, son aquellas queries que modifican datos en BBDD (por ejemplo un POST, PUT o DELETE en un típico CRUD). Si tenemos una lista de libros que obtenemos con un GET, la edición, adición o borrado de un libro serían mutaciones sobre la lista de libros. Estas mutaciones, no tienen ninguna key al no tener que mantener en caché ningún dato, puesto que son acciones que se realizarían puntualmente. Entonces, el hook useMutation en lugar de recibir la key, recibe directamente la función que realiza la mutación, y una configuración adicional.

Un caso de uso que contiene queries y mutaciones sería el siguiente:

Tenemos una tabla con proyectos en la DB y un CRUD básico en el Backend. Si tenemos en Front un listado y por ejemplo una creación, podríamos tener estas dos queries:

Por un lado la query que se trae los proyectos:

const { data, isLoading, error } = useQuery("GetProjects", getProjects);

El funcionamiento dentro de un componente de React es muy sencillo. React Query por defecto, va a realizar una petición en un componente al montarse cuando esté utilizando un hook como useQuery. Utilizando la query anterior, vemos que nos da un estado de isLoading y al resolverse, nos dará o bien un data o un error. El componente volverá a renderizar cuando cambie uno de estos parámetros y tendremos ese manejo ya controlado de forma automática!

Y por otro lado el método para crear proyectos:

const [createProject] = useMutation(service.createProject, {
  onSuccess: () => queryCache.invalidateQueries("GetProjects"),
});

Podemos vincular el primer parámetro del array que nos devuelve con la acción a realizar (probablemente con algún onClick) y fijaos lo que está pasando. Estamos utilizando una mutation, pasándole la función que va a “mutar” los datos que no controlamos, y luego le pasamos qué hacer en el caso en el que la petición haya ido correctamente en el onSuccess. Y lo que le estamos diciendo en ese onSuccess es que ejecute una función que va a invalidar la query con el nombre ’GetProjects’. Automáticamente si detecta que hay una query invalidada, vuelve a pedir los datos, con lo que se vuelve a repetir el flujo de antes y no haría falta gestionar ese estado de “refresh” tampoco.

Un caso un poco más específico

Pues bien, después de conocer por encima qué serían las queries y qué serían las mutaciones, en mi primera implementación de React Query, vi el caso de uso que tenía delante:

  • Una tabla que muestra datos (una query paginada).
  • Acciones de la tabla a nivel de fila y de toda la tabla (mutaciones sobre los datos).

Qué requisitos debe cumplir nuestra implementación?

  • Debe manejar un estado complejo de la tabla
  • Debe manejar cancelaciones.
  • Debe manejar un dato que se obtiene en la primera request, para enviar en peticiones sucesivas.

El componente de tabla que usamos es un componente propio, que nos hace tener que manejar un estado en el componente que lo usa, para guardar ciertos datos (filtrado, paginación, pageSize).

Además para estas peticiones de tabla, necesitamos un parámetro extra que Backend nos devuelve en la primera petición, y que enviaremos en las peticiones sucesivas. Si este parámetro cambiara, tendremos que enviarlo en la siguiente petición y así (temas de caché).

El primer approach fue utilizar una query paginada, añadiendo a la key la paginación, el pageSize y los filtros. Como puedes crear tus propios hooks con React Query, en principio cada query tendrá su propio hook.

Ahora tenemos que añadir el tema de la cancelación y el manejar un dato, así que decidí crear mi hook para manejar todo eso de forma especial para cada petición que tenga que ver con una tabla:

let myParam;
export function useGetMyTableDataQuery(tableState) {
  // Create a new AbortController instance for this request
  const controller = new AbortController();
  // Get the abortController's signal
  const signal = controller.signal;
  return usePaginatedQuery(
    [
      Queries.QueryName,
      tableState.page,
      tableState.pageSize,
      tableState.filters,
    ],
    () => {
      const promise = service.fetchMyTableData(
        { ...tableState, param: myParam },
        signal,
      );
      // Cancel the request if React Query calls the `promise.cancel` method
      promise.cancel = () => controller.abort();
      return promise.then((resolvedData) => {
        myParam = resolvedData.myParam;
        return resolvedData;
      });
    },
  );
}

De momento para controlar el tema del parámetro que tenemos que guardar para siguientes peticiones, lo haremos a través de una closure (pregunta de examen), guardando el resultado en myParam.

Las acciones que modifican filas o la tabla completa, no tienen más complejidad que la mostrada en el ejemplo con useMutation anterior. Simplemente invalidan la query, o en algunos casos, varias queries (no importa si invalida algunas que no están en pantalla, puesto que no las va a pedir).


Contenido extra


Si te ha gustado este post, ¡tengo para ti otra buena noticia! Estoy preparando un curso de React Query que subiré a una plataforma de e-learning y me gustaría saber tu opinión. El curso será en inglés, pero si te interesa tenerlo en español, házmelo saber vía email, o directamente por Twitter. Si quieres además contenido que voy encontrándome por mi camino, no olvides suscribirte a mi newsletter!

Un saludo!