Optimiza la gestión de datos en tu aplicación Vue con 'Queries y Mutations' (Vue Query)

Si no estás familizarizado con Vue Query, te recomiendo primero leer el artículo de Introducción a Vue Query

Uno de los principales retos a los que se enfrentan los desarrolladores es la gestión de datos, especialmente en el frontend. Lograr sincronizar los datos entre el cliente y el servidor puede ser un dolor de cabeza. En este artículo, exploraremos cómo Vue Query integra el patrón de 'queries' y 'mutations' para facilitar y acelerar esta tarea.

En este artículo aprenderás:

  1. ¿Qué es el patrón Queries & Mutations?
  2. ¿Cómo funciona Vue Query (@tanstack/vue-query)?
  3. Ejemplo con Queries y Mutations

¿Qué es el patrón Queries & Mutations?

De manera general podemos diferenciar entre 'queries' y 'mutations' como peticiones de lectura y escritura en nuestra aplicación. En el contexto de Vue Query, las queries son peticiones de lectura que se encargan de obtener datos de nuestro backend a través de una API RESTful y las mutations se encargan de modificar esos datos en el servidor. En el desarrollo de aplicaciones web modernas, la gestión del estado de la información es un aspecto crítico para el funcionamiento de la aplicación. Uno de los desafíos que enfrentamos los desarrolladores es asegurar la sincronización entre el estado de la información en el servidor y el cliente. Cuando se trata de la gestión del estado de la información, existen dos enfoques principales: 'server state' y 'client state'.

Server state y Client state

El 'server state' se refiere al estado de la información almacenada en el servidor. Por otro lado, el 'client state' se refiere al estado de la información almacenada en la memoria del cliente, es decir, la aplicación que se está ejecutando en el navegador del usuario.

La sincronización entre el 'server state' y el 'client state' es fundamental para asegurar que la información que se muestra en la aplicación sea precisa y actualizada. Por ejemplo, si un usuario realiza un cambio en la información en un dispositivo, la aplicación debe reflejar ese cambio en todos los dispositivos que están conectados a la misma cuenta del usuario.

Sincronización entre frontend y backend

Conforme crece la complejidad de las aplicaciones, la gestión de datos se vuelve un desafío. De manera tradicional lo que haríamos para sincronizar los datos entre el cliente y el servidor es hacer una petición de lectura cada vez que se realice un cambio en la información. Sin embargo, este enfoque no es muy eficiente, ya que cada vez que se realice un cambio en la información, se debe hacer una petición de lectura para obtener la información actualizada.

¿Y que pasa si estamos utilizando los datos actualizados en otras partes de la aplicación? ¿Cómo podemos asegurarnos de que toda la aplicación esté utilizando la información actualizada? Podríamos utilizar un gestor de estado como Vuex o Pinia, pero tendríamos que hacer las actualizaciones manualmente cada que la petición de escritura regrese de manera exitosa. Además de eso tendríamos que ser capaces de manejar los errores de manera adecuada, ya que si la petición de escritura falla, la información que se muestra en la aplicación no será la misma que la que se encuentra en el servidor.

Es muy fácil imaginar como esto puede volverse un dolor de cabeza a medida que la aplicación crece, pero gracias a @tanstack/vue-query, simplificar y optimizar la gestión y sincronización de datos entre frontend y backend nunca fue más fácil.

¿Cómo funciona Vue Query (@tanstack/vue-query)?

La base para entender cómo funciona por detrás esta librería es entender el concepto de caching. El caching es una técnica de almacenamiento en memoria que permite almacenar datos en el navegador para que puedan ser utilizados más tarde sin tener que volver a hacer una petición al servidor. En el contexto de Vue Query, el caching se utiliza para almacenar los datos que se obtienen de las queries y las mutations. En el cache podemos almacenar datos de diferentes peticiones.

Por ejemplo: En una aplicación de ecommerce, podemos almacenar los datos de los productos que se muestran en la página principal, los datos de los productos que se muestran en la página de categorías, los datos de los productos que se muestran en la página de favoritos, etc.

¿Pero como sabemos cuales datos pertenecen a cada petición? Para esto, utilizamos el concepto de llave única . Cada petición de lectura tiene un identificacdor único que nos va a permitir acceder a sus datos y tambien a modificarlos (mutarlos). Para el ejemplo anterior, tendríamos 3 llaves únicas: 'productos', 'categorias' y 'favoritos'. Digamos que la petición de /api/favoritos devolvió los siguientes datos:

{
  "data": [
    {
      "id": 1,
      "nombre": "Producto 1",
      "precio": 100,
      "categoria": "categoria 1"
    },
    {
      "id": 2,
      "nombre": "Producto 2",
      "precio": 200,
      "categoria": "categoria 2"
    },
    {
      "id": 3,
      "nombre": "Producto 3",
      "precio": 300,
      "categoria": "categoria 3"
    }
  ]
}

* Ese objeto va a estar almacenado en el cache con la llave 'favoritos'.

Si el usuario encuentra otro producto que le gusta y hace clic a 'agregar a favoritos', tendríamos que modificar (mutar) el cache que pertenece a la llave de 'favoritos'. En lugar de hacer la petición de escritura a la base de datos y luego refrescar los nuevos favoritos, podemos hacer la petición de escritura y aprovechar la respuesta de esa petición para modificar directamente el cache que ya teníamos anteriormente. De esta manera no tenemos que hacer una petición de lectura para obtener los nuevos favoritos, ya que tenemos todos en el cache menos el útlimo agregado.

Ejemplo Práctico de Queries y Mutations

Para instalar Vue Query desde cero consulta el artículo Introducción a Vue Query

Ejemplo: Vamos a desarrollar un sistema para poder agregar productos de un ecommerce a favoritos utilizando Vue Query. Para este ejemplo, vamos a utilizar los siguientes endpoints:

// GET /api/favorites/:userId

const ejemploRespuestaGET = {
  data: [
    {
      id: 1,
      nombre: "Producto 1",
      precio: 100,
      categoria: "categoria 1",
    },
    {
      id: 2,
      nombre: "Producto 2",
      precio: 200,
      categoria: "categoria 2",
    },
    {
      id: 3,
      nombre: "Producto 3",
      precio: 300,
      categoria: "categoria 3",
    },
  ],
};

// POST /api/favorites/

const ejemploRespuestaPOST = {
  id: 4, // id del nuevo producto favorito
  nombre: "Producto 4", // nombre del nuevo producto favorito
  precio: 400, // precio del nuevo producto favorito
  categoria: "categoria 4", // categoría del nuevo producto favorito
};

Query

Hook:
// hooks/useFavoriteProducts.js import {useQuery} from "@tanstack/vue-query";

// Custom Hook para la query de favoritos
export const useFavoriteProducts = (userId) => {
  const queryKey = "favoritos";

  const fetchFavorites = async () => {
    const response = await fetch(`/api/favorites/${userId}`);
    const data = await response.json();
    return data?.data; // accedemos a la propiedad data del objeto que nos regresa la petición anterior
  };

  const { data, isLoading, error } = useQuery([queryKey], fetchFavorites);

  return {
    favorites: data,
    loadingFavorites: isLoading,
    errorFavorites: error,
  };
};

@tanstack/vue-query nos permite crear queries y manejar muchísimos estados de las peticiones de manera muy sencilla. En el ejemplo anterior, podemos ver que la query tiene 3 estados:

  1. data: los datos que se obtuvieron de la petición de lectura exitosa.
  2. isLoading: un booleano que nos indica si la petición de lectura se está ejecutando.
  3. error: un booleano que nos indica si la petición de lectura falló.

Existen muchos otros estados que podemos administrar 'out of the box' con esta librería, los puedes consultar aquí.

Componente:
<!-- FavoriteProducts.vue -->
<template>
  <div>
    <h1>Favoritos</h1>
    <div v-if="loadingFavorites">Cargando...</div>
    <div v-else-if="errorFavorites">Error</div>
    <div v-else-if="favorites">
      <ul>
        <li v-for="favorite in favorites" :key="favorite.id">
          {{ favorite.nombre }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
  import { useFavoriteProducts } from "./hooks/useFavoriteProducts";

  const user = getUser(); // Obtener el usuario
  const { favorites, loadingFavorites, errorFavorites } = useFavoriteProducts(
    user.id
  );
</script>

Mutation

Para nuestro ejemplo, vamos a hacer un hook que nos permita agregar un producto a los favoritos. Haríamos lo mismo para hacer un hook que se encargue de quitar un producto.

Hook:
// hooks/useAddFavorite.js
import { useMutation } from "@tanstack/vue-query";

export const useAddFavoriteMutation = () => {
  const queryKey = "favoritos"; // Esta llave única tiene que ser la misma que utilizamos para obtener los datos que queremos mutar.

  const addFavorite = async (productId, userId) => {
    const response = await fetch("/api/favorites", {
      method: "POST",
      body: JSON.stringify({ productId, userId }),
      headers: {
        "Content-Type": "application/json",
      },
    });
    const data = await response.json();
    return data;
  };

  const {mutate, isLoading, error} = useMutation(addFavorite, {
    [queryKey],
    onSuccess: (newFavorite) => {
      // Es este caso, newFavorite es la respuesta de la petición de escritura que incluye el nuevo producto.
      queryClient.setQueryData(["favoritos"], (old: any) => {
        // old es el valor que teníamos en el cache antes de hacer la petición de escritura.
        return [...old, newFavorite];
      });
    },
    onError: (error) => {
      // Manejar el error
    },
  });

  return {
    addFavorite: mutate,
    loadingAddFavorite: isLoading,
    errorAddFavorite: error,
  };
};
Componente:
<!-- FavoriteProducts.vue -->
<template>
  <div>
    <h1>Todos los productos</h1>
    <div v-if="allProducts">
      <ul>
        <li v-for="product in allProducts" :key="product.id">
          <button @click="()=> addFavorite(product.id, user.id)">
            agregar a favoritos
          </button>
          <span>{{ product.nombre }}</span>
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
  import { useAddFavoriteMutation } from "./hooks/useAddFavoriteMutation";

  const user = getUser(); // Obtener el usuario
  const { allProducts } = useAllProducts(); // Hook para obtener todos los productos, (solo para el ejemplo)
  const { addFavorite, loadingAddFavorite, errorAddFavorite } =
    useAddFavoriteMutation();
</script>

Cuando el usuario hace clic en el botón 'agregar a favoritos', se ejecuta la mutación y se actualiza el cache de 'favoritios' con el nuevo producto favorito agregado. Automaticamente cuando el usuario navega a la página de favoritos, el cache ya está actualizado y no tenemos que hacer una petición de lectura para obtener los nuevos favoritos. De cualquier manera, la librería constantemente hace peticiones por detrás para siempre asegurar que el cache esté actualizado.

Ayúdame a mejorar este artículo

¿Quisieras complementar este artículo o encontraste algún error?¡Excelente! Envíame un correo.

  • seb@sebastianfdz.com