
Loading Service en Vue/Nuxt 3
Algunas formas de implementar un servicio global de loading con Vue/Nuxt y PrimeVue
Para crear un servicio global de loading que permita gestionar fácilmente los estados de carga y controlar un spinner de carga, exploraremos diferentes enfoques usando Vue/Nuxt versión 3 con la API de Composición, la sintaxis <script setup>
, y el componente ProgressSpinner
de la biblioteca PrimeVue 3.
Primera Aproximación: Composable
El primer enfoque consiste en crear un composable que exporte un servicio básico de loading. Comenzaremos creando un composable con una lógica sencilla de carga: una variable reactiva isLoading
para gestionar el estado global de loading, y dos métodos, startLoading()
y stopLoading()
.
//useLoading.ts
import { ref } from 'vue';
export function useLoading() {
const isLoading = ref(false);
const startLoading = () => {
isLoading.value = true;
};
const stopLoading = () => {
isLoading.value = false;
};
return {
isLoading,
startLoading,
stopLoading,
};
}
Ejemplo de Implementación en un Componente:
//example.vue
<template>
<div>
<ProgressSpinner v-if="isLoading" aria-label="Loading" />
<Home v-if="!isLoading" :products="products" />
</div>
</template>
<script setup>
import { useLoading } from '~/composables/useLoading';
import { onMounted } from 'vue';
import Home from '~/components/Home.vue'; // Assuming Home component exists
const { startLoading, stopLoading, isLoading } = useLoading();
onMounted(async () => {
startLoading();
try {
// Simulating fetching data
const products = await fetchProducts();
} catch (error) {
console.error('Error fetching products:', error);
} finally {
stopLoading();
}
});
async function fetchProducts() {
// Simulating API call
return new Promise((resolve) => {
setTimeout(() => {
resolve(['Product1', 'Product2']);
}, 2000);
});
}
</script>
Este enfoque gestiona directamente el estado de loading dentro del componente. Aunque es algo más detallado, encapsula la lógica principal, separa las responsabilidades y es accesible globalmente. Es un buen punto de partida, pero podemos optimizarlo.
Segunda Aproximación: Patrón provide/inject
El patrón provide/inject
en Vue 3 permite compartir estados entre un componente padre y sus descendientes, pasando datos y métodos por el árbol de componentes sin necesidad de prop drilling.
Vamos a crear un composable useLoading.ts
usando este patrón, junto con un componente LoadingOverlay
para representar la interfaz de loading.
Composable useLoading.ts
//composables/useLoading.ts
import { ref, provide, inject } from 'vue';
interface LoadingContext {
isLoading: Ref<boolean>;
setLoading: (value: boolean) => void;
}
const LoadingSymbol = Symbol('loading');
export function provideLoading(): void {
const isLoading = ref(false);
function setLoading(value: boolean) {
isLoading.value = value;
}
provide(LoadingSymbol, {
isLoading,
setLoading,
});
}
export function useLoading(): LoadingContext {
const loading = inject(LoadingSymbol);
if (!loading) {
throw new Error('No loading provider found');
}
return loading as LoadingContext;
}
Crear el Componente LoadingOverlay:
<template>
<div v-if="isLoading" class="flex justify-content-center>
<ProgressSpinner />
</div>
</template>
<script setup>
import { useLoading } from '~/composables/useLoading';
const { isLoading } = useLoading();
</script>
Implementación de useLoading
en un componente hijo:
<template>
<Home v-if="!isLoading && products.length" :products="products" />
</template>
<script setup>
import { useLoading } from '~/composables/useLoading';
const { setLoading, isLoading } = useLoading();
// Simulating fetch logic
onMounted(async () => {
try {
setLoading(true);
const products = await fetchProducts();
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
});
async function fetchProducts() {
// Simulating API call
return new Promise((resolve) => {
setTimeout(() => {
resolve(['Product1', 'Product2']);
}, 2000);
});
}
</script>
Proveer el Estado de Loading en un Componente de Nivel Superior (por ejemplo, app.vue):
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
<script setup>
provideLoading();
</script>
Esto garantiza que cualquier componente pueda acceder al estado global de loading utilizando useLoading()
.
Tercera Aproximación: Usar Broadcasting de Eventos
Si necesitas más flexibilidad para transmitir el estado de loading entre componentes que no son padre/hijo, puedes usar un event bus como mitt. Esto permite que componentes no relacionados escuchen cambios en el estado de loading sin tener que pasar datos por el árbol de componentes.
Comienza instalando la librería mitt:
npm install mitt
yarn add mitt
pnpm add mitt
Crear un Servicio Global de Loading (useLoadingBroadcast.ts
):
//composables/useLoadingBroadcast.ts
import { ref } from 'vue';
import mitt from 'mitt';
type LoadingEvents = {
'loading:change': boolean;
};
const emitter = mitt<LoadingEvents>();
const isLoading = ref(false);
export function useLoadingBroadcast() {
function setLoading(value: boolean) {
isLoading.value = value;
emitter.emit('loading:change', value);
}
function onLoadingChange(callback: (value: boolean) => void) {
emitter.on('loading:change', callback);
}
function offLoadingChange(callback: (value: boolean) => void) {
emitter.off('loading:change', callback);
}
return {
isLoading,
setLoading,
onLoadingChange,
offLoadingChange,
};
}
Crear un Componente LoadingOverlay
:
//loading-overlay.vue
<template>
<div class="loading-overlay" v-if="isLoading">
<ProgressSpinner />
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { useLoadingBroadcast } from '~/composables/useLoadingBroadcast';
const { onLoadingChange, offLoadingChange } = useLoadingBroadcast();
const isLoading = ref(false);
const handleLoadingChange = (value) => {
isLoading.value = value;
};
onMounted(() => {
onLoadingChange(handleLoadingChange);
});
onUnmounted(() => {
offLoadingChange(handleLoadingChange);
});
</script>
Usar LoadingOverlay
en un Componente de Nivel Superior (por ejemplo, default.vue):
//default.vue
<template>
<LoadingOverlay />
<slot />
</template>
Implementación en un Componente Hijo:
//example.vue
<template>
<div>
<Home v-if="!isLoading" :products="products" />
</div>
</template>
<script setup>
import { useLoadingBroadcast } from '~/composables/useLoadingBroadcast';
import { onMounted } from 'vue';
import Home from '~/components/Home.vue'; // Assuming Home component exists
const { setLoading, isLoading } = useLoadingBroadcast();
onMounted(async () => {
try {
setLoading(true);
const products = await fetchProducts();
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
});
async function fetchProducts() {
// Simulating API call
return new Promise((resolve) => {
setTimeout(() => {
resolve(['Product1', 'Product2']);
}, 2000);
});
}
</script>
Este setup garantiza que:
- La función
setLoading
emite un evento cuando se llama. - El
LoadingOverlay
escucha estos eventos y actualiza el estado de loading en consecuencia. - Cualquier componente puede controlar el estado global de loading.
Conclusión
Implementar un servicio global de loading en Vue/Nuxt 3 se puede abordar de varias maneras, dependiendo de la complejidad y estructura de tu aplicación. El enfoque de Composable es ideal para un uso a pequeña escala con un overhead mínimo, mientras que el patrón de Provide/Inject ofrece una solución más escalable para árboles de componentes padre-hijo. Para una mayor flexibilidad, especialmente cuando se trata de componentes no relacionados, el enfoque de Broadcasting de Eventos utilizando mitt proporciona una forma eficiente de gestionar el estado de loading en toda la aplicación. Cada método tiene sus ventajas, permitiéndote adaptar la solución a las necesidades de tu proyecto.