React Query (Tanstack) para realizar peticiones a APIs
React Query, ahora llamada Tanstack, es una librería para realizar peticiones a una API, que trabaja en conjunto con Fetch o Axios por ejemplo. Con React Query, tenemos muchas funcionalidades integradas y de fácil uso para hacer las peticiones, guardar la información en caché, sincronizar y actualizar el estado del servidor.
Con React Query puedes manejar múltiples peticiones simultaneas, controlar los estados de los datos en carga y automáticamente re-solicitar información desactualizada cuando sea necesario.
También tiene capacidades de realizar "actualizaciones optimistas" (optimistic updates) para acelerar y mejorar la experiencia de usuario. Estas actualizaciones optimistas, se refieren a la capacidad de predecir el comportamiento o estado de los datos antes de recibir la confirmación desde el servidor de que la afirmación es correcta. En el caso de fallo en el servidor se realiza un roll back y los datos se devuelven al estado anterior.
Por último esta librería nos ofrece sincronización en background, por lo tanto puede ir actualizando los datos mientras el usuario está haciendo uso de la aplicación y de forma transparente sin que se tenga que solicitar expresamente.
Preparando el entorno
Para el ejemplo vamos a usar React + Vite con Typescript.
Instalamos React y Vite con el siguiente comando:
npm create vite@latest
Eliges el nombre del proyecto, la opción de React y Typescript. Luego accedes al directorio que se ha creado e instalas las dependencias:
npm install
Una vez finalizado, ejecutas
npm run dev
Si todo está correcto se mostrará la URL para acceder al servidor.
Ahora toca instalar la librería Tanstak React Query:
npm install @tanstack/react-query
Si ya tienes algo de conocimientos, lo ideal sería tener una API, así que si ya tienes un servidor de backend puedes adaptar las peticiones a los datos que tienes. Otra opción es usar una API pública, que hay muchas con tipoas de datos muy variados. En el caso de que no tengas experiencia vamos a simular el acceso a un API con llamadas al backend con un array.
Con esto ya tenemos todo el entorno preparado para realizar nuestro proyecto.
Creación de los componentes
Para este ejemplo vamos a necesitar acceder a pocos archivos ya que lo vamos a mantener sencillo, así que obviaremos toda la parte del diseño del front end, simplemente mostraremos los datos como una lista y crearemos un input con un botón para guardar nueva información.
La aplicación, para ser "muy originales", será una lista de notas, una todo list de toda la vida, donde mostraremos las tareas ya almacenadas en nuestra base de datos (array) y añadiremos nuevas.
Si ya has usado o tienes conocimientos de Fetch o Axios, las peticiones todas se hacen de la misma manera, lo que cambia es el verbo HTTP (GET, POST, PUT, DELETE) que le especificamos y la incrustación de datos en el caso que la petición lo requiera.
En React Query existen dos tipos de peticiones diferentes:
- query: Son todas las peticiones que requieren obtener datos desde el servidor (GET).
- mutation: Son todas las peticiones que modifican el estado del servidor de alguna forma, ya sea, creando (POST), actualizando (PUT) o borrando (DELETE).
Lo siguiente es crear el contexto que ofrece React Query para que todos los componentes puedan acceder a sus funcionalidades.
Esto lo puedes hacer en el archivo donde lo vayas a necesitar, en el caso que quieras que estos componentes que ofrece la librería estén disponibles para todos los componentes de la aplicación, deberás hacerlo o en App.tsx o en main.tsx. Yo prefiero hacerlo en este último y así no se llena el archivo App cuando la aplicación va creciendo en funcionalidad y necesita mas componentes de jerarquía alta.
Archivo: /src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css';
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>,
)
Lo primero es importar los componentes QueryClient y QueryClientProvider, este último es el proveedor de contexto, que habilita el acceso a las funcionalidades (hooks) que ofrece React Query. Y QueryClient, es el objeto principal de la librería.
Como con cualquier proveedor de contexto, ya sea de librerías o un contexto creado por nosotros, envolvemos con el componente todos los otros componentes (hijos) que queremos que tengan acceso a él, en nuestro ejemplo, el objeto <App />, que corresponde a toda la aplicación, por lo tanto todos los componentes dentre de él tendrán también acceso al contexto que ofrece <QueryClientProvider client={queryClient} />.
Como prop del proveedor, le pasamos client={queryClient} que hemos instanciado previamente usando: new QueryClient(). Con esto ya tenemos todos listo para usar React Query en nuestra aplicación.
Preparación de los datos
Vamos a crear un fichero con la estructura de datos (array) que contendrá objetos tarea (todo), con los campos: id y title.
Archivo: /src/data.ts
const todos = [
{
id: 1,
title: 'Tarea 1',
},
{
id: 2,
title: 'Tarea 2',
},
{
id: 3,
title: 'Tarea 3',
},
{
id: 4,
title: 'Tarea 4',
},
{
id: 5,
title: 'Tarea 5',
},
];
Con 5 tareas es mas que suficiente. Ahora preparamos nuestra fake API con las funciones de recuperación de las tareas y añadir una nueva tarea.
Archivo: /src/api/api.ts
import './data.ts';
export const fetchTodos = async (query = "") => {
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simular tiempo de espera de una API real
return todos;
}
export const addTodo = async (title) => {
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulamos asincronía
const newTodo = {
id: todos.length + 1, // Creamos un nuevo id, con la longitud del array + 1
title: title,
}
todos.push(newTodo); // Añadimos la nueva tarea al array
return todos[todos.length - 1]; // Devolvemos el último elemento del array, la nueva tarea añadida
}
Queries
Como ya hemos comentado, las queries son para recuperar datos desde la API, así vamos a implementar una función para extraer todas las tareas del array todos. Y por el bien de la simplicidad lo vamos a hacer todo en el archivo App.tsx
Archivo: /src/App.tsx
import { useQuery } from "@tanstack/react-query";
import { fetchTodos } from "./api/api";
function App() {
const { isLoading, isError, data: todos } = useQuery({
queryFn: () => fetchTodos(),
queryKey: ['todos'],
});
if(isLoading) return <div>Loading...</div>;
if(isError) return <div>Error!</div>;
return (
<>
{
todos.map(todo => (
<div key={todo.id}>
{todo.title}
</div>
))
}
</>
)
}
export default App;
Vamos a analizar el fichero:
- Primero las importaciones, vamos a importar el hook useQuery de React Query y fetchTodos de nuestra fake API.
- Hacemos uso del useQuery(), y desestructuramos varias propiedades que aporta:
- isLoading: Es un booleano que tiene el valor true mientras está cargando los datos y false, cuando ya se han cargado.
- isError: Es un booleano que obtiene el valor true si ha habido algún error al ejecutar la petición.
- data: En esta propiedad, es donde están los datos, como puedes ver tiene un alias = todos (Typescript: darle otro nombre a la misma variable), para que sea mas descriptivo, no confundir con una clave valor de un objeto.
3. Le pasamos un objeto como propiedad a useQuery con las siguientes propiedades:
- queryFn: Fn => Function, por lo que esta es la función que se ejecutará para esta petición, en el caso del ejemplo: fetchTodos().
- queryKey: Este es un array donde primero se especifica el nombre identificador de la petición "todos". Esto sirve luego para poder modificar esta petición desde otras, y también es el espacio en memoria que reserva React Query para la caché de esta petición.
4. Después hacemos uso de las propiedades isLoading e isError, para mostrar contenido si alguna de las dos está en true.
5. Por último pintamos los valores en pantalla usando la función .map del array todos.
Y ya tenemos nuestra sencilla lista de tareas con título y contenido.
Mutations
Con las mutations conseguimos modificar el estado del servidor a través de las acciones HTTP que ya hemos mencionado. Pero también tenemos la posibilidad de interactuar con otras peticiones usando la propiedad de queryKey.
Archivo: /src/App.tsx
import { useQuery, useMutation } from "@tanstack/react-query";
import { fetchTodos, addTodo } from "./api/api";
function App() {
const { isLoading, isError, data: todos } = useQuery({
queryFn: () => fetchTodos(),
queryKey: ['todos'],
});
if(isLoading) return <div>Loading...</div>;
if(isError) return <div>Error!</div>;
const { isPending, mutateAsync: addTodoMutation } = useMutation({
mutationFn: addTodo,
});
const [ title, setTitle ] = useState('');
const handleChange = (e) => {
setTitle(e.target.value);
}
const handleClick = async () => {
try {
await addTodoMutation(title);
setTitle('');
} catch(error) {
throw new Error(error.message);
}
}
return (
<>
<div>
<div>
<input type="text"
onChange={handleChange}
value={title}
/>
</div>
<div>
<button onClick={handleClick}>Sumit</button>
</div>
</div>
{
todos.map(todo => (
<div key={todo.id}>
{todo.title}
</div>
))
}
</>
)
}
export default App;
Vamos a analizar el código que hemos añadido debido al uso de useMutation:
- Ahora tenemos dos importaciones mas, useMutation y addTodo.
- Hemos creado un estado con useState para guardar el título de la tarea que introducimos en el input.
- Para manejar el cambio ocurrido en el input, tenemos la función handleChange, que modifica la variable de estado title, con la función setTitle, y así siempre está actualizada.
- Necesitamos otro manejador (handler) para cuando se pulsa el botón de submit para guardar una nueva tarea, con handleClick ejecutamos la mutation que llama a la función addTodoMutation con la variable de estado title como argumento. Todo en un bloque try catch para capturar excepciones. Y por último pone la variable title en blanco de nuevo.
- Al igual que con useQuery desestructuramos propiedades de useMutation:
- isPending: Es igual que isLoading, pero a la espera que de recibir confirmación de la mutation por parte del servidor.
- mutationAsync: Con el alias addTodoMutation (puede ser cualquier nombre), esta es la función que se va a llamar para ejecutar la mutation.
6. Por último los componentes que pintamos:
- input: Tiene como value el varlo contenido por la variable de estado title, y en su propiedad onChange llama a handleChange.
- button: Con la propiedad onClick llama a handleClick que inicia la ejecución de la mutation.
Si te gusta el contenido que te comparto, puedes apoyarme haciendo una aportación! :) https://buymeacoffee.com/raulalhena
Manejo de la caché
La memoria caché es donde React Query almacena datos de forma temporal para tenerlos accesibles de forma mas rápida y también poder mostrarlos sin necesidad de hacer una petición al servidor, o, primero mostrarlos y después realizar la petición al servidor, de esta manera mejora la experiencia a la hora de visualizar los datos.
Con la propiedad queryKey que hemos definido en las propiedades de useQuery(), podemos controlar la información que se almacena en la caché. El valor es un array de elementos de los que depende si esta caché es modificada o no. Por lo que podemos pasarle tanto una cadena de caracteres que usaremos como identificador de esa petición en concreto, y también otros elementos: variables, objetos, etc, que si se modifican, generará un cambio en la caché.
const query = useQuery({
queryFn: <función>,
queryKey: [<nombre_de_query>]
})
A parte de la dependencia de los cambios en alguna propiedad como sería las dependencias de useEffect, también la podemos modificar con intención desde otra petición o mutation, haciendo uso de la propiedad onSuccess, que como valor tiene una función donde podemos ejecutar código, y la función invalidateQueries([]) del objeto QueryClient que obtenemos con el hook useQueryClient de React Query.
const queryClient = useQueryClient();
const { isPending, mutateAsync: newTodo } = useMutation({
mutationFn: addTodo,
onSuccess: () => {
queryClient.invalidateQueries([<nombre_de_query>])
}
});
Por defecto te muestra la información en la caché y en el background vuelve a realizar la petición al servidor y de esta forma actualiza la caché. Usando la propiedad staleTime puedes definir el tiempo que quieres que React Query considere que los datos han caducado y tiene que volver a solicitarlos al servidor. Podrías usar el valor Infinity y de esta forma nunca actualizaría los datos de la caché. El valor por defecto es 0.
Con la propiedad cacheTime, también podemos indicarle el tiempo que queremos que mantenga la caché, o que por ejemplo, no haga uso de ella en absoluto pasándole el valor 0, por lo tanto, React Query realizará una petición nueva al servidor cuando se necesiten los datos de la petición en concreto o de forma global cuando instanciamos QueryClient:
const queryClient = new QueryClient({
defaultOptions: {
staleTime: 10*(60*1000), // 10 min
cacheTime: 5*(60*1000) // 5 min
}
}
El valor por defecto de cacheTime son 5 minutos.
La librería dispone de muchos otros métodos y propiedades para el manejo de la caché y así personalizar el comportamiento a tus necesidades, pero hemos mostrado algunas para no hacer el artículo demasiado largo.
Conclusiones
Haciendo uso de esta librería tienes mucha parte del trabajo hecha a cambio de unos ajustes iniciales para adaptarte a la manera de funcionar de React Query. Tiene otras utilidades que pueden encajar bien en tu proyecto, puedes consultarlas en su documentación: https://tanstack.com/
Esta librería tiene sus limitaciones y para proyectos de mas envergadura puedes usar Redux por ejemplo, que te da más capacidad en la gestión de peticiones y de comunicación entre componentes.
¡Felicidades, ya sabes usar la librería de React Query para interactuar con API!