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.
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í.
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.