É 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: []
}
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
}
}
}
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
}
}
}
}
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>
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>
import UsersFetcher from '@/components/UsersFetcher'
export default {
name: 'UsersList',
components: {
UsersFetcher
}
}
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>
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
}
}
}
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>
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
}
}
}
}
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>
import Fetcher from '@/components/Fetcher'
import { fetchUsers } from '@/services/users'
export default {
name: 'UsersList',
components: {
Fetcher
},
methods: {
fetchUsers
}
}
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
Top comments (0)