Cómo (y cuándo) compartir propiedades reactivas en los composables

Aprende la diferencia entre las funciones compositoras stateless y stateful de Vue 3


Uno de los usos más comunes para las funciones compositoras de Vue 3 es hacer de almacén de estado global. Pero no tiene que ser siempre así, también existen usos útiles para composables sin estado global.

Antes de continuar, un stateless composables no significa que no tenga estado, sino que ese estado vive dentro de la función compositora y por eso se recrea en cada ejecución. No se comparte con el resto de instancias del composable.

Stateless composable

Como siempre, todo es más sencillo de entender con un ejemplo.

Aquí tienes useAsync,un composable para gestionar llamadas asíncronas (forma parte del contenido relacionado con composables del Taller de Composition API.

useAsync.tsTS
import { ref, type UnwrapRef } from 'vue';
export async function useAsync<T>(fn: () => Promise<T>) {
  // Local state
  const loading = ref(false);
  const error = ref<Error | null>(null);
  const result = ref<T | null>(null);
  try {
    result.value = (await fn()) as UnwrapRef<T>;
  } catch (e) {
    if (e instanceof Error) {
      error.value = e;
    }
  } finally {
    loading.value = false;
  }
  return {
    loading,
    error,
    result,
  };
}

Como ves, las piezas de estado reactivas loading, error y result viven dentro de la función useAsync. Esto quiere decir que con cada ejecución de useAsync se crearán de nuevo: no se compartirán.

Son propiedades de estado local.

Esto tiene todo el sentido del mundo, ya que por defecto no parece interesante la idea de sobreescribir result con el resultado de cada operación asíncrona. Lo mismo con el estado de carga (loading) y los posibles mensajes de error. Dicho de otra forma: conviene empezar desde cero cada vez.

Stateful composable

Supongamos ahora que queremos añadir una opción para almacenar todos los errores —un log— de las operaciones asíncronas. Además, ofrecer una función para eliminarlos.

Dónde crees que debería vivir esta nueva funcionalidad, ¿dentro o fuera de useAsync?.

Por supuesto, fuera. Esta vez sí queremos que el log de errores sea global y compartido. Algo así.

useAsync.tsTS
import { ref, type UnwrapRef } from 'vue';
// Global state
export const errors = ref<string[]>([]);
export const resetErrors = () => (errors.value = []);
////
interface Options {
  recordError?: boolean;
}
export async function useAsync<T>(fn: () => Promise<T>, options: Options = {}) {
  const { recordError = true } = options; // Options pattern
  const loading = ref(false);
  const error = ref<Error | null>(null);
  const result = ref<T | null>(null);
  try {
    result.value = (await fn()) as UnwrapRef<T>;
  } catch (e) {
    if (e instanceof Error) {
      error.value = e;
      recordError && errors.value.push(e.message);
    }
  } finally {
    loading.value = false;
  }
  return {
    loading,
    error,
    result,
  };
}

Como puedes ver, errors y resetErrors viven fuera de useAsync, pero se consumen dentro (fíjate en el bloque catch). Esto es perfecto para llevar el control global de todas las operaciones que no han funcionado, por cualquier razón.

En este caso, son propiedades de estado global.

Conclusión

Aquí puedes comprobar como en un mismo composable pueden convivir en armonía piezas de estado reactivo globales y locales. Cada una sirve a una causa diferente. Sí, en ambos casos queremos ser notificados cuando ocurran mutaciones (para eso son reactivas), la clave es el ámbito: global o stateful vs local o stateless.

Demo

Juan Andrés Núñez
Juan Andrés Núñez
Ingeniero Frontend. Especialista en Vue.js. Docente profesional. Estoico.