DEV Community

Cover image for VueJS - Componente Reutilizável de Carregamento de Dados
Pablo Veiga
Pablo Veiga

Posted on

3

VueJS - Componente Reutilizável de Carregamento de Dados

É possível contar com os dedos de uma mão as aplicações web ao redor do mundo que não precisam realizar carregamento de dados remotos e exibi-los aos usuários.

Então, assumindo que a sua próxima Single Page Application (construída usando VueJS, logicamente 😍) vai precisar obter dados de um servidor remoto, eu gostaria de te ensinar a construir um componente reutilizável que vai ser responsável por gerenciar a visualização de estado de outros componentes que dependem de carregamento de dados e prover, facilmente, feedback para seus usuários.

Começando pelo começo

Inicialmente, é preciso ter em mente o quão importante é a exibição correta do estado atual da aplicação para que os usuários saibam o que está acontecendo e o que esperar dela.
Isso vai fazer com que eles não fiquem em dúvida se a interface travou enquanto esperam informações serem carregadas e também informá-los caso ocorra algum erro para que possam entrar em contato com o suporte imediatamente, se necessário.

Padrão Loading / Error / Data (Carregamento / Erro / Dado)

Eu não tenho certeza se é um padrão "oficial" (me mande uma mensagem caso você saiba algo a respeito) mas esta é uma forma muito fácil de implementar e que vai ajudar você a organizar a exibição do estado da sua aplicação de forma bastante simples.

Considere o objeto abaixo. Ele representa o estado inicial de uma lista de users (usuários):

const users = {
  loading: false,
  error: null,
  data: []
}
Enter fullscreen mode Exit fullscreen mode

Ao construir objetos neste formato, você poderá alterar o valor de cada atributo de acordo com o que está acontecendo na sua aplicação e utilizá-los para exibir na tela qualquer coisa de acordo com cada estado por vez. Portanto, quando a aplicação estiver carregando os dados, basta setar loading para true e quando o carregamento for concluído, setar para false.

De forma similar, error e data também devem ser atualizados de acordo com o resultado da chamada ao back end: se algum erro ocorreu, você pode atribuir a mensagem ao atributo error e, caso a requisição tenha sido concluída e o dado entregue com sucesso, basta atribuí-lo ao atributo data.

Especializando

Um objeto de estado, como explicado acima, ainda é muito genérico. Vamos inseri-lo no contexto de uma aplicação VueJS.
Faremos isso implementando um componente utilizando slots, o que vai nos permitir passar o dado recebido pelo componente Fetcher para os componentes filho.

De acordo com a documentação do VueJS:

Vue implementa uma API de distribuição de conteúdo que é modelada após o atual detalhamento da especificação dos componentes da Web, usando o elemento <slot> para servir como saída de distribuição de conteúdos.

Para iniciar, crie uma estrutura básica de um componente Vue e implemente o objeto users como variável reativa dentro de data conforme o exemplo abaixo:

export default {
  data() {
    return {
      loading: false,
      error: null,
      data: null
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Agora, crie o método responsável por fazer o request, carregar os dados e atualizar a variável de estado. Perceba que fazemos a chamada ao método que carrega os dados no hook created para que seja executado assim que o componente for criado.

import { fetchUsers } from '@/services/users'

export default {
  data() {
    return {
      loading: false,
      error: null,
      data: []

    }
  },
  created() {
    this.fetchUsers()
  }
  methods: {
    async fetchUsers() {
      this.loading = true
      this.error = null
      this.users.data = []

      try {
        fetchUsers()
      } catch(error) {
        this.users.error = error
      } finally {
        this.users.loading = false
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

O próximo passo é implementar o template que irá exibir elementos diferentes de acordo com os estados de Loading (carregando), Error (erro) e Data (dados) usando slots para passar o valor de data para componentes filhos, caso esteja definido.

<template>
  <div>
    <div v-if="users.loading">
      Loading...
    </div>
    <div v-else-if="users.error">
      {{ users.error }}
    </div>
    <slot v-else :data="users.data" />    
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Com o componente Fetcher construído, vamos utilizá-lo em outro componente chamado UsersList, que irá representar nossa lista de usuários.

<template>
   <UsersFetcher>
     <template #default="{ data }">
       <table>
         <tr>
           <th>ID</th>
           <th>Name</th>
           <th>Age</th>
         </tr>
         <tr v-for="user in data" :key="user.id">
           <td>{{ user.id }}</td>
           <td>{{ user.name }}</td>
           <td>{{ user.age }}</td>
         </tr>
       </table>
     </template>
   </UsersFetcher>
</template>
Enter fullscreen mode Exit fullscreen mode
import UsersFetcher from '@/components/UsersFetcher'

export default {
  name: 'UsersList',
  components: {
    UsersFetcher
  }
}
Enter fullscreen mode Exit fullscreen mode

Tornando o componente reutilizável

Esta foi uma forma muito simples de se implementar o padrão Loading / Error / Data a fim de capturar e exibir feedback correto para os usuários quando a aplicação precisa buscar dados remotos. Porém, a implementação acima não é muito reutilizável já que está carregando e manipulando, estritamente, usuários.

Para tornar o componente mais genérico, basta implementarmos algumas pequenas mudanças e assim será possível utilizá-lo em qualquer lugar onde nossa aplicação precise buscar e exibir dados.

Primeiro, vamos tornar o componente Fetcher mais dinâmico visto que, em uma aplicação real, teremos que carregar diversos tipos de dados que, por sua vez, requerem métodos de serviço e nomes de variáveis específicos.
Vamos utilizar props para passar valores dinâmicos para dentro do componente.

<template>
  <div>
    <div v-if="loading">
      Loading...
    </div>
    <div v-else-if="error">
      {{ error }}
    </div>
    <slot v-else :data="data" />    
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode
export default {
  name: 'Fetcher',
  props: {
    apiMethod: {
      type: Function,
      required: true
    },
    params: {
      type: Object,
      default: () => {}
    },
    updater: {
      type: Function,
      default: (previous, current) => current
    },
    initialValue: {
      type: [Number, String, Array, Object],
      default: null
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Analisando cada uma das props definidas acima:

apiMethod [obrigatória]: a função responsável por realizar a chamada à API para carregar dados externos

params [opcional]: os parâmetros enviados na chamada do método de serviço (apiMethod), quando necessários. Ex.: quando precisamos carregar dados usando filtros.

updater [opcional]: função que irá transformar os dados recebidos.

initialValue [opcional]: o valor inicial do atributo data do objeto de estado.

Após implementar estas props, vamos criar agora o mecanismo principal que irá permitir que o componente seja reutilizado.
Utilizando as props definidas, podemos agora definir as operações e controlar o estado do componente de acordo com o resultado da requisição.

<template>
  <div>
    <div v-if="loading">
      Loading...
    </div>
    <div v-else-if="error">
      {{ error }}
    </div>
    <slot v-else :data="data" />    
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode
export default {
  name: 'Fetcher',
  props: {
    apiMethod: {
      type: Function,
      required: true
    },
    params: {
      type: Object,
      default: () => {}
    },
    updater: {
      type: Function,
      default: (previous, current) => current
    },
    initialValue: {
      type: [Number, String, Array, Object],
      default: null
    }
  },
  data() {
    return {
      loading: false,
      error: null,
      data: this.initialValue
    }
  },
  methods: {
    fetch() {
      const { method, params } = this
      this.loading = true

      try {
        method(params)
      } catch (error) {
        this.error = error
      } finally {
        this.loading = false
      }
    }
  } 
}
Enter fullscreen mode Exit fullscreen mode

Após implementar estas mudanças, assim ficará o nosso componente Fetcher:

<template>
   <Fetcher :apiMethod="fetchUsers">
     <template #default="{ data }">
       <table>
         <tr>
           <th>ID</th>
           <th>Name</th>
           <th>Age</th>
         </tr>
         <tr v-for="user in data" :key="user.id">
           <td>{{ user.id }}</td>
           <td>{{ user.name }}</td>
           <td>{{ user.age }}</td>
         </tr>
       </table>
     </template>
   </Fetcher>
</template>
Enter fullscreen mode Exit fullscreen mode
import Fetcher from '@/components/Fetcher'
import { fetchUsers } from '@/services/users'

export default {
  name: 'UsersList',
  components: {
    Fetcher
  },
  methods: {
    fetchUsers
  }
}
Enter fullscreen mode Exit fullscreen mode

E é isso! :)
Utilizando apenas conceitos básicos de VueJS como props e slots podemos criar um componente de carregamento de dados reutilizável que será responsável por carregar e exibir os dados e prover feedback apropriado conforme o estado da aplicação.
Além disso, você pode utilizá-lo em qualquer página ou componente que precise carregar dados, independentemente do tipo.

Você encontra um exemplo 100% funcional desta implementação neste repositório.

Espero que tenha gostado. Por favor, comente e compartilhe!

Gostaria de agradecer especialmente a Neil Merton por ter me ajudado a corrigir partes do código utilizado neste artigo.

Imagem de capa por nordwood

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs