Loading Service em Vue/Nuxt 3

lgumas abordagens para um serviço global de loading em Vue/Nuxt e PrimeVue

Catarina Roseta • 10 de setembro de 2024


layout: post title: "Loading Service em Vue/Nuxt 3" date: 2024-09-10 author: Catarina Roseta category: Ferramentas de Desenvolvimento tags: - vue3 - Nuxt3 - PrimeVue3 abstract: Algumas abordagens para um serviço global de loading em Vue/Nuxt e PrimeVue image: assets/img/posts/2024-09-10-use-loading.png

Para criar um serviço global de loading que permita gerir facilmente estados de loading e controlar um indicador de carregamento, podemos explorar diferentes abordagens, aqui utilizaremos Vue/Nuxt v3 com composition API, setup na tag script (<script setup>) e o componente ProgressSpinner da biblioteca PrimeVue v3.

Primeira Abordagem: Composable

A primeira abordagem envolve a criação de um composable que exporta um serviço de loading básico. Começaremos por criar um composable com lógica simples: um ref booleano isLoading para gerir o estado global de loading e dois métodos, startLoading() e 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,
  };
}

Exemplo de Implementação num 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>

Esta abordagem gere diretamente o estado de loading dentro do componente. Embora seja um pouco mais verbosa, encapsula a lógica central, separa as responsabilidades e é acessível globalmente. É um bom ponto de partida, mas podemos tornar mais eficiente.


### Segunda Abordagem: provide/inject Pattern

O pattern provide/inject no Vue 3 permite partilhar estado entre um componente pai e os seus descendentes, passando dados e métodos pela árvore de componentes sem a necessidade de "prop drilling".

Vamos criar um composable useLoading.ts usando o provide/inject pattern, juntamente com um componente LoadingOverlay para representar a interface de loading.

Criação do 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;
}

Criaçao do 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>

Implementação do useLoading num Componente Filho:

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

Fornecendo o Estado de Loading num Componente de Nível Superior (por exemplo, app.vue)

    <template>
      <NuxtLayout>
        <NuxtPage />
      </NuxtLayout>
    </template>
    
    <script setup>
      provideLoading();
    </script>

Esta abordagem garante que qualquer componente possa acessar o estado global de loading usando o useLoading().

Terceira Abordagem: Broadcasting de Eventos

Se precisar de mais flexibilidade para transmitir o estado de loading entre componentes que não são pai/filho, pode usar um event bus como mitt. Isto permite que componentes não relacionados ouçam alterações no estado de loading sem passar dados pela árvore de componentes.

Comece por instalar a biblioteca mitt:

`npm install mitt`  
`yarn add mitt`  
`pnpm add mitt`

Criar um Serviço 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,
      };
    }

Criar um 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 num Componente de Nível Superior (por exemplo, no layout default.vue):

//default.vue

    <template>
        <LoadingOverlay />
        <slot />
    </template>

Implementação num Componente Filho:

//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 garante que:

  1. A função setLoading emite um evento quando chamada.
  2. O LoadingOverlay ouve esses eventos e atualiza o estado de loading de acordo.
  3. Qualquer componente pode controlar o estado global de loading.

Conclusão

Implementar um serviço global de loading no Vue/Nuxt 3 pode ser feito de várias maneiras, dependendo da complexidade e estrutura da sua aplicação. A abordagem Composable é ideal para uso em pequena escala com overhead mínimo, enquanto o pattern Provide/Inject oferece uma solução mais escalável para árvores de componentes pai-filho. Para maior flexibilidade, especialmente ao lidar com componentes não relacionados, a abordagem de Broadcasting de Eventos usando mitt fornece uma maneira eficiente de gerir o estado de loading em toda a aplicação. Cada método tem as suas vantagens, permitindo adaptar a solução às necessidades do projeto.


Últimos artigos relacionados