
Loading Service in Vue/Nuxt 3
A few approaches to a global loading service using Vue/Nuxt and PrimeVue
To create a global loading service that allows you to easily manage loading states and control a loading spinner, we’ll explore different approaches using Vue/Nuxt v3 with
the Composition API, the <script setup>
syntax, and the PrimeVue v3 library's ProgressSpinner
component.
First Approach: Composable
The first approach involves creating a composable that exports a basic loading service. We'll start by creating a composable with simple loading logic: a reactive
variable isLoading
to manage the global loading state, and two methods, startLoading()
and 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,
};
}
Implementation Example in a Component:
//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>
This approach directly manages the loading state within the component. While it's a bit more verbose, it encapsulates the core logic, separates concerns, and is globally accessible. It’s a good starting point, but we can optimize it.
Second Approach: Using provide/inject
Pattern
The provide/inject
pattern in Vue 3 allows you to share state between a parent component and its descendants, passing data and methods down the component tree without explicit
prop drilling.
We'll create a useLoading.ts
composable using this pattern, alongside a LoadingOverlay
component to represent the loading UI.
//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;
}
This creates a globally shared loading state accessible by components lower in the tree.
Creating the LoadingOverlay
Component
<template>
<div v-if="isLoading" class="flex justify-content-center>
<ProgressSpinner />
</div>
</template>
<script setup>
import { useLoading } from '~/composables/useLoading';
const { isLoading } = useLoading();
</script>
Implementing the useLoading
in a Child Component:
<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>
Providing the Loading State in a High-Level Component (e.g., app.vue
):
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
<script setup>
provideLoading();
</script>
This ensures any component can access the global loading state using useLoading()
.
Third Approach: Using Event Broadcasting
If you need more flexibility to broadcast the loading state across components that aren't parent/child, you can use an event bus
like mitt
. This allows unrelated components to listen for loading state changes without passing data through the
component tree.
Start by installing the mitt
library:
`npm install mitt`
`yarn add mitt`
`pnpm add mitt`
Create a Global Loading Service (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,
};
}
Create a 'LoadingOverlay` Component:
//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>
Use LoadingOverlay
in a High-Level Component (e.g.default.vue
):
//default.vue
<template>
<LoadingOverlay />
<slot />
</template>
Implementing in a child component:
//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>
This setup ensures:
- The
setLoading
function emits an event when called. - The
LoadingOverlay
listens for these events and updates the loading state accordingly. - Any component can control the global loading state.
Conclusion
Implementing a global loading service in Vue/Nuxt 3 can be approached in several ways, depending on the complexity and structure of your application. The Composable approach is
ideal for small-scale use with minimal overhead, while the Provide/Inject pattern offers a more scalable solution for parent-child component trees. For greater flexibility,
especially when dealing with unrelated components, the Event Broadcasting approach using mitt
provides a clean way to manage the loading state across your app. Each method
has its strengths, allowing you to tailor your solution to your project’s needs.