Aunque Redux ha sido una herramienta básica en el desarrollo con React durante los últimos años, las nuevas características del core de React como context y hooks prometen ser un sustituto capaz e incluso mejor.

Sin embargo, estos cambios se introdujeron hace ya años y la transición no ha sido tan rápida como se podría esperar. Una de las razones podría ser que el flujo de datos es una parte tremendamente arquitectural en una aplicación y no puede sustituirse por otra librería o API aleatoriamente sino que debe establecerse una metodología consistente, cómoda y útil para su uso en toda la aplicación.

Conforme la tecnología de hooks va madurando, empiezan a aparecer nuevas librerías basadas en ellos y que sí pueden proponerse como sustitución total del flujo de datos. La que veremos en este artículo es React Query, que nos ofrece una serie de ventajas que su autor Tanner Linsley nos resume como:

De todas destacaría cacheo, control de datos caducados y la composición de una especie de estado global que es consultable desde cualquier punto de la aplicación.

Comparando una arquitectura con la otra, vemos que la principal diferencia arquitectural es que React Query abstrae el entramado de acciones/reductores/sagas o thunks de forma que nuestros componentes puedan interactuar con la librería de una forma más lineal, con el beneficio de escribir menos código.

Otra diferencia arquitectural es que Redux nos animaba a almacenar entidades o datos puros en el global store para luego consultarlos como si fuera una base de datos local. Con React Query se nos pide que abandonemos las entidades y nos fijemos únicamente en las llamadas o queries. A ellas reaccionarán ahora los componentes.

Configuración

Instalamos React Query con yarn add react-query.

Disponemos de una herramienta de visualización que nos permitirá ver qué queries están activas, caducadas o en proceso y que nos será muy útil en el desarrollo (previa instalación del paquete de dev tools con yarn add react-query-dev-tools --dev).

// App.js
import React from ‘react’;
import { ReactQueryDevTools } from ‘react-query-devtools’;

export default () => (
  <div className=”app”>
    …
    <ReactQueryDevtools initialIsOpen />
  </div>
);

De esta manera dispondremos de una consola dedicada a React Query como esta:

Creando y consumiendo una query

Basándonos en un ejemplo clásico como sería una lista de contactos, vamos a crear nuestro primer componente que consuma una query. Nos pondremos como referencia el código de este repositorio donde se encuentra este ejemplo completamente funcional pero en los ejemplos obviaremos funcionalidades no relacionadas como animaciones, mocks/servicios, rutas, etc.

// api/getContact.js
export default ({ id }) => (
  fetch(`/contact?id=${id}`)
  .then(res => res.json())
);

Es recomendable abstraer las llamadas en una carpeta aparte (llamada api, por ejemplo) para aumentar la claridad y reutilización. En este caso usamos fetch, pero podríamos usar otras librerías como Axios o Apollo.

// src/screens/ContactDetail.js
import React from 'react';
import { useQuery } from 'react-query';
import { Link, useRouteMatch } from 'react-router-dom';
import { getContact } from '../api'

export default () => {
  const { params: { id } } = useRouteMatch();
  const { data: contact } = useQuery(
    ['contact', id],
    async () => await getContact({ id }),
  );

  return (
    <div>
      <Link to="/">Back</Link>
      <h1>CONTACT DETAIL</h1>
      {
        contact &&
        (
    <div className="contact-card">
  <img alt={contact.name} src={contact.avatar} />
  {contact.name}
  {contact.city}, {contact.country.code}
  {contact.phone}
  {contact.email}
      </div>
        )
      }
    </div>
  );
};

Aquí empezamos a ver las queries en uso y lo sencillas y breves que pueden llegar a ser. UseQuery es el hook que utilizaremos más a menudo, que recibe como parámetros un índice (que suele ser un string pero puede ser cualquier objeto de Javascript) y una llamada asíncrona que ya definimos en el fichero anterior. Lo que recibimos al completar la llamada, es devuelto a través del atributo data y se convierte en el dato que actualizará el componente. El índice debe escogerse con cuidado ya que es el identificador que usa React Query para decidir qué hacer con la llamada, por ejemplo usando datos cacheados o haciendo otra nueva. En este caso es apropiado usar [‘contact’, id] porque la llamada es propia y diferente de cada contacto que se quiera consultar.

Los beneficios que hemos obtenido usando React Query, en lugar de hacer la llamada por nuestra cuenta, son el cacheado, discernido de si se debe hacer una llamada nueva, precarga de datos si es una llamada antigua mientras se lanza otra de actualización… y seguramente alguna operación más que la librería hace por nosotros. Es un paquete de gestión de llamadas que requiere muy poco esfuerzo por nuestra parte.

Si este u otro componente necesitan consumir esta llamada en el futuro, también esta operación será controlada por React Query de la forma más eficiente y sin intervención por nuestra parte.

Scroll infinito

Uno de los recursos más utilizados en web (sobre todo en desarrollo para móvil) es el del scroll que muestra una lista infinita que se va actualizando según el usuario va bajando o paginando. React Query también tiene un hook para eso que podemos utilizar en el listado general.

Podemos definir la nueva llamada que necesitaremos así:

// api/listContacts.js
export default ({ page = 0 } = {}) => (
  fetch(`/contacts?page=${page}`)
  .then(res => res.json())
);

Y el componente de listado así:

// List view component
import React from 'react';
import { useInfiniteQuery } from 'react-query';
import { Link } from 'react-router-dom';
import InfiniteScroll from 'react-infinite-scroll-component';
import { listContacts } from '../api';

export default () => {
  const {
    data,
    fetchMore,
    canFetchMore,
  } = useInfiniteQuery(
    'contacts',
    async (_, page) =>  await listContacts({ page }),
    { getFetchMore: data => data.page + 1 },
  );

  return (
    <div>
      <h2 className="contacts-list__title">CONTACT LIST</h2>
      <InfiniteScroll
        dataLength={data ? data.length * 20 : 0}
        next={() => fetchMore()}
        hasMore={canFetchMore}
        loader={LOADING}
        endMessage={END}
      >
      {
        data && data.map((page, i) => (
          page.contacts.map(contact => (
            <div
              className="contact-list-item"
              key={contact.id}
             >
    <Link to={`/contact/${contact.id}`}>
      <span>{contact.name}</span>
      <span>{contact.city}</span>
    </Link>
  </div>

          ))
        ))
      }
      </InfiniteScroll>
      </CountProvider>
    </motion.div>
  );
};

El componente de scroll infinito que utilizamos en este caso es react-infinite-scroll-component aunque podríamos usar seguramente cualquier otro.

En lo referente a React Query, vemos que useInfiniteQuery (al que asignamos la etiqueta ‘contacts’) acepta un callback con parámetros (page) y devuelve adicionalmente una función fetchMore que cambia con cada llamada (para tener un atributo page actualizado cada vez) y configuraremos para que el scroll infinito use cuando el usuario requiera más datos.

También disponemos de canFetchMore, que se mantiene a true mientras la llamada anterior haya devuelto datos. Podemos consultar la api de estos hooks, algunos más dedicados a cosas específicas, en esta completísima documentación.

Precarga de datos cacheados

Hasta ahora hemos visto nuestro nuevo recurso, las queries, como algo atómico, aisladas unas de otras. Sin embargo, esto no es así: están todas persistidas y unificadas bajo un objeto común llamado queryCache y que podemos consultar con el hook useQueryCache.

Una de las funciones que podemos darle en nuestra aplicación de ejemplo puede ser que, al navegar del listado al detalle de un contacto, se precarguen inicialmente los datos de ese ítem en el listado para mostrar el dato rápidamente al usuario (aunque sea parcial) y mientras tanto pedir los datos reales de detalle al servicio de back. Para ello, debemos volver a abrir la vista de detalle y la dejaremos así:

// Detail view component
import React from 'react';
import { useQuery, useQueryCache } from 'react-query';
import { Link, useRouteMatch } from 'react-router-dom';
import { getContact } from '../api';

export default () => {
  const { params: { id } } = useRouteMatch();
  const queryCache = useQueryCache();
  const { data: contact } = useQuery(
    ['contact', id],
    async () => await getContact({ id }),
    {
      initialData: () => queryCache
        .getQueryData('contacts')
        ?.map(page => page.contacts)
        .flat()
        .find(contact => contact.id === Number(id))
      ,
    },
  );

  return (
    <div>
      <Link to="/">Back</Link>
      <h1>CONTACT DETAIL</h1>
      {
        contact &&
        (
    <div className="contact-card">
  <img alt={contact.name} src={contact.avatar} />
  {contact.name}
  {contact.city}, {contact.country.code}
  {contact.phone}
  {contact.email}
      </div>
        )
      }
    </div>
  );
};

El cambio que hemos realizado en ese punto es acceder al objeto queryCache, buscar en él el registro de listado que identificamos con el id de usuario y, si lo encontramos, lo insertamos como dato inicial de la respuesta de dicha query, que puede mientras tanto pedirse en segundo plano y actualizarse cuando se disponga del dato real.

Configuración

Hemos hecho referencia ya varias veces a toda la funcionalidad que React Query hace por nosotros automáticamente pero es muy posible que queramos configurarla o desactivarla parcialmente. Para nuestro caso, no queremos que las queries se refresquen cuando el usuario quita y luego devuelve el foco a la pestaña del navegador y tampoco queremos que las queries caduquen al momento de haber sido hechas. Para ello, podemos crear un archivo de configuración (más opciones de configuración aquí):

// reactQueryConfig.json
{
  "queries": {
    "refetchOnWindowFocus": false,
    "staleTime": 180000
  }
}

Y luego importarlo en la raíz de la aplicación con ReactQueryConfigProvider:

import React from 'react';
import { ReactQueryDevtools } from 'react-query-devtools';
import { ReactQueryConfigProvider } from 'react-query';
import reactQueryConfig from './reactQueryConfig.json';

export default () => {
  return (
    <div className="app">
      <ReactQueryConfigProvider config={reactQueryConfig}>
    ...
      </ReactQueryConfigProvider>
      <ReactQueryDevtools initialIsOpen />
    </div>
  );
}

React Query es una librería grande y completa y, echándole un vistazo más detenido a su API, veremos que las opciones son amplias tanto para una configuración general como para cada una de las llamadas. Por toda la funcionalidad que abstrae por nosotros y de la que se ocupa automáticamente, es el tipo de librería que se presta a construir una arquitectura entera a su alrededor en la que podamos despreocuparnos del origen de datos y simplemente importarlos en los componentes con un hook.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.