DEV Community

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

Posted on

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

Discussion (0)