In questo articolo configureremo un loader globale che si attiva quando qualunque richiesta GraphQL è in corso e si disattiva quando tutte le richieste sono terminate.
Useremo un'istanza di ApolloLink
per contare le richieste attive e uno store reattivo Vue per mostrarne lo stato nell’interfaccia.
Prerequisiti
npm i @apollo/client graphql
Struttura dei file
src/
apollo/
activityLink.js
client.js
components/
Loader.vue
stores/
apolloLoading.js
App.vue
Store reattivo: contatore richieste attive
Creiamo uno store minimale con un contatore e un computed per sapere se c’è almeno una richiesta in corso.
// src/stores/apolloLoading.js
import { ref, computed } from "vue";
const active = ref(0);
export const apolloActiveRequests = {
inc() { active.value = Math.max(0, active.value + 1); },
dec() { active.value = Math.max(0, active.value - 1); },
active, // espone il numero esatto
isLoading: computed(() => active.value > 0), // true se >= 1 richiesta
};
2) Apollo Link: incrementa/decrementa il contatore
Il link comprende forward(operation)
e gestisce i casi complete, error e unsubscribe, evitando loader bloccanti.
// src/apollo/activityLink.js
import { ApolloLink, Observable } from "@apollo/client/core";
import { apolloActiveRequests } from "@/stores/apolloLoading";
export const activityLink = new ApolloLink((operation, forward) => {
// opzionale: escludi alcune richieste dal loader globale
if (operation.getContext().silent) {
return forward(operation);
}
apolloActiveRequests.inc();
return new Observable((observer) => {
const sub = forward(operation).subscribe({
next: (value) => observer.next(value),
error: (err) => { apolloActiveRequests.dec(); observer.error(err); },
complete: () => { apolloActiveRequests.dec(); observer.complete(); },
});
return () => { sub.unsubscribe(); apolloActiveRequests.dec(); };
});
});
Client Apollo
Qui non usiamo la funzione from()
, deprecata, ma ApolloLink.from([...])
.
// src/apollo/client.js
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
import { ApolloLink } from "@apollo/client/core";
import { activityLink } from "./activityLink";
const httpLink = new HttpLink({ uri: "/graphql" });
const link = ApolloLink.from([
activityLink, // deve stare PRIMA per intercettare tutto
httpLink,
]);
export const apollo = new ApolloClient({
link,
cache: new InMemoryCache(),
});
Componente Vue: barra di caricamento globale
Mostriamo/nascondiamo la barra in base allo stato dello store. Gli stili CSS sono puramente indicativi.
<!-- src/components/Loader.vue -->
<script setup>
import { computed } from "vue";
import { apolloActiveRequests } from "@/stores/apolloLoading";
const isLoading = computed(() => apolloActiveRequests.isLoading.value);
</script>
<template>
<div :class="{'visible': isLoading}" class="loader"></div>
</template>
<style>
.loader {
position: fixed;
left: 0;
right: 0;
top: 0;
height: 3px;
background: linear-gradient(90deg, #bbb, #333, #bbb);
background-size: 200px 3px;
animation: glide 1s linear infinite;
z-index: 9999;
display: none;
}
.loader.visible {
display: block;
}
@keyframes glide {
from { background-position: 0 0; }
to { background-position: 200px 0; }
}
</style>
Inserimento nel layout dell'applicazione
Inseriamo il loader globale una sola volta, ad esempio nel root component.
<!-- src/App.vue -->
<script setupt>
import { RouterView } from "vue-router";
import Loader from "@/components/Loader.vue";
</script>
<template>
<Loader />
<RouterView />
</template>
Note e consigli
- Ordine dei link: l’
activityLink
va prima dell’HttpLink
per intercettare tutto. - Anti-flash: se il loader lampeggia per richieste brevissime, applica un piccolo ritardo nel mostrare la barra (debounce di 150–250 ms) con un
watch
sullo store. - Errori/abort: decrementiamo il contatore su complete, error e unsubscribe per non lasciare il loader in uno stato non conforme allo step corrente dell'attività di Apollo Client.
- SSR/Non-browser: evita il CSS del loader globale lato server.