Desenvolvimento

18 fev, 2019

Vuex: padronizando o gerenciamento de estado da sua aplicação Vue.js

Publicidade

Quando estamos desenvolvendo aplicações com Vue.js é comum realizarmos comunicações de um componente pai com componentes filhos (ou vice-versa). E algumas vezes, vários componentes utilizam os mesmos dados entre eles.

Conforme a aplicação cresce, torna-se complexo manusear esses dados e mantê-los atualizados em todos os componentes, ou acaba se tornando entendiante passar os dados para componentes profundamente aninhados.

Para componentes irmãos essa estratégia não funcionaria, sendo então necessário atualizar os componentes através de eventos. Só que a longo prazo e dependendo do tamanho da aplicação o código se torna insustentável.

O que é Vuex?

Segundo a sua documentação, o Vuex é um padrão de gerenciamento de estado + biblioteca para aplicativos Vue.js. Ele serve como um store centralizado para todos os componentes em uma aplicação, com regras garantindo que o estado só possa ser mudado de forma previsível.

E ele também se integra com a extensão oficial Vue devtools para fornecer recursos avançados sem configurações adicionais, como depuração viajando pelo histórico de estado (time travel) e exportação/importação de registros de estado em determinado momento.

Mas o que é um Padrão de Gerenciamento do Estado?

Vamos reproduzir um aplicativo simples de contador Vue:

Exemplo contador Vue

Este é um aplicativo independente com as seguintes partes:

  • Estado (state): fonte da verdade que direciona nosso aplicativo
  • View: um mapeamento declarativo do estado
  • Ações (actions): possíveis maneiras pelas quais o estado pode mudar em reação às interações dos usuários da view

Abaixo vemos uma representação extremamente simples do conceito de “fluxo de dados unidirecional” (one-way):

Fonte: https://vuex.vuejs.org/ptbr/

A simplicidade é quebrada rapidamente quando temos vários componentes que compartilham um estado em comum. Vamos imaginar que múltiplos componentes dependam do mesmo pedaço de estado do exemplo acima: neste caso, estariam dependendo do count. E ações de diferentes views podem precisar mudar o mesmo pedaço de estado.

Já notamos rapidamente algumas dificuldades como, por exemplo, passar propriedades (props) para componentes profundamente aninhados. Porém, essa estratégia não funcionaria para componentes irmãos.

Já falando das ações para mudanças do estado, a solução seria alcançar referências diretas da instância pai/filho ou tentar alterar e sincronizar várias cópias do estado por meio de eventos.

E então, surge o Vuex

O Vuex extrai o estado compartilhado dos componentes e o gerencia em um singleton global. Com isso, a árvore de componentes se torna uma grande “view”, e qualquer componente pode acessar o estado ou acionar ações, não importando onde elas estejam na árvore.

Com tal separação e definição, os conceitos envolvidos no gerenciamento do estado, com a aplicação de certas regras, transforma nosso código em algo mais estruturado e com sustentabilidade.

A ideia básica por trás do Vuex foi inspirada por Flux, Redux, e The Elm Architecture. Ao contrário de outros padrões, o Vuex também é uma implementação de biblioteca adaptada especificamente para o Vue.js tirar proveitos de seus sistema de reatividade granular para atualizações eficientes.

Quando usar o Vuex?

Embora o Vuex nos ajude a lidar com o gerenciamento de estado compartilhado, ele também vem com o custo de mais conceitos e códigos repetitivos. É uma escolha de prós e contras entre produtividade de curto e longo prazo.

Particularmente, hoje utilizo o Vuex em todos os projetos desenvolvidos com Vue e Quasar, até nos projetos simples. Essa escolha é levada em conta de que um projeto simples tende a se tornar mais complexo conforme vamos adicionando novos recursos.

Se você está iniciando no mundo Vue, criando suas primeiras SPA’s e for direto para o Vuex, certamente ele poderá parecer detalhado e desanimador.

Isso é perfeitamente normal, mas a tendência natural é que em algum momento situações encontradas no decorrer do desenvolvimento venham a caminhar para o gerenciamento de estado compartilhado.

Uma citação de Dan Abramov, autor do Redux, torna isso ainda mais real:

  • “As bibliotecas Flux são como óculos: você saberá quando precisar delas.”

Criando um projeto com Vuex

Para entender na prática o funcionamento do Vuex, criaremos um pequeno projeto com o Quasar Framework.

Para isso, o ambiente de desenvolvimento já deve possuir o vue-cli e quasar-cli instalados globalmente.

Executamos o comando abaixo em seu terminal favorito:

quasar init vuex-quasar

O comando acima irá criar um projeto através do quasar-cli com o nome vuex-quasar. Iremos escolher as seguintes configurações para nosso projeto:

Criando um projeto com quasar-cli

Com nosso projeto criado, podemos executar o comando para levantar um servidor local para o ambiente de desenvolvimento:

quasar dev

Ao analisarmos a estrutura do nosso projeto notamos que ele já possui um diretório dentro de src chamado store. Como configuramos na criação de nosso projeto o Vuex, ele já prepara nosso projeto para trabalhar com ele.

Módulos

Devido ao uso de uma única árvore de estado, todo o estado de nossa aplicação está contido dentro de um grande objeto. No entanto, à medida em que nosso aplicativo cresce em escala, o store pode ficar realmente inchado.

Para uma melhor organização, o Vuex nos permite dividir nosso store em módulos. Cada módulo pode conter seu próprio state, mutation, action, getters, e até módulos aninhados.

E por padrão, o quasar já cria nosso projeto com módulos, tornando-o mais organizado. Analisar nosso index.js do diretório store. Veja como é feita a importação dos módulos para a Store do Vuex:

index.js da store

Namespacing

Por padrão, actions, mutations e getters dentro dos módulos ainda são registrados sob o namespace global – isso permite que vários módulos reajam ao mesmo tipo de ação/mutação.

Porém se quisermos que os módulos sejam mais independentes ou reutilizáveis, podemos marcá-los como namespaced com namespaced:true.

Ou seja, quando o módulo é registrado, todos os getters, actions e mutations serão automaticamente namespaced com base no caminho no qual o módulo está registrado.

Essa configuração já vem no módulo de exemplo da store do Quasar. Podemos ver isso no arquivo index.js do módulo module-example.

index.js do module-example

Estado (state)

Como nossa store está modularizada, cada módulo irá possuir seu state. Primeiramente vamos criar um state no module-example chamado count.

Em nossa page Index.vue, podemos obter o state do módulo example da seguinte forma:

Sempre que o store.state.example.count mudar, o dado computado será reavaliado, e se necessário, atualizará o DOM associado a ele.

O Auxiliar mapState

Quando um componente precisa fazer uso de várias propriedades do state da store, declarar esses dados computados pode ser repetitivo e verboso. Para lidar com isso podemos usar o auxiliar mapState que gera funções getter computadas para nós, economizando algumas linhas de código.

Porém, para usá-lo com outros dados computados locais, teríamos que usar um utilitário para fundir vários objetos em um para passarmos o objeto final para a computed. Para simplificar isso, usamos o Spread Operator, deixando nosso mapeamento de state da seguinte forma:

mapState

Observe que, como primeiro parâmetro, passamos o nome do módulo, e depois um array com os states que serão mapeados para nosso componente.

Em nossa aplicação já é possível ver que o dado foi realmente mapeado de forma correta e renderizado no DOM:

Aplicação com o state exibido.

Lembrando que, apesar de estarmos utilizando o Vuex, não significa que você deve sair colocando TUDO no estado do Vuex. Embora colocar mais estado no Vuex torna suas mutações de estado mais explícitas e depuráveis, ás vezes também pode tornar o código mais verboso e indireto. Se um pedaço de estado pertence estritamente a um único componente, pode ser bom deixá-lo apenas como um estado local.

Getters

Algumas vezes vamos precisar tratar nosso estado antes de renderizá-lo em nosso componente. Para isso, podemos utilizar os getters. Por exemplo, queremos filtrar através de uma lista de itens e contá-los – em nosso state iremos criar uma lista de usuários:

State com array de usuários

Em nosso arquivo getter.js do module-example vamos fazer o seguinte filter e calcular a quantidade de usuários com o status true:

getter.js

Em nossa page Index.vue, mapeamos nosso getter quase que da mesma forma que fizemos como state, porém, agora usando o mapGetters e renderizamos ele em nosso DOM:

E por fim, nossa aplicação mostra o resultado renderizado:

Resultado em nossa aplicação quasar.

Mutações (Mutations)

Em uma store Vuex, a única maneira de realmente mudar o estado é utilizando o commit de uma mutação. As mutações do Vuex são muito semelhantes aos eventos: cada mutação tem uma cadeia de caracteres tipo e um manipulador.

É um padrão comumente visto usar constantes para tipos de mutação em várias implementações do Flux. Isso permite que o código aproveite as ferramentas como os linters.

Construiremos duas mutações: uma para incrementar nosso estado count e outra para decrementar. Nosso arquivo de mutation dentro do diretório store/module-example ficará dessa forma:

mutation module-example

As mutations também podem receber um argumento adicional chamado payload. Na maioria dos casos o payload é um objeto contendo informações (por exemplo, uma resposta de uma API) para serem passadas para o estado.

Agora, em nossa page Index.vue adicionamos dois botões e o mapMutation para mudarmos o estado do count.

Index.vue com mapMutations

Nossa aplicação agora tem dois botões que alteram o estado do nosso contador.

Aplicação com incrementar e decrementar contador

No Vuex, as mutações são transações síncronas. Para lidar com operações assíncronas, podemos utilizar as Ações(actions).

Ações (Actions)

As ações são semelhantes às mutações. As diferenças são as seguintes:

  • Em vez de mudar o estado, as ações confirmam (ou fazem commit de) mutações
  • As ações podem conter operações assíncronas arbitrárias

Registraremos duas actions simples, que vão commitar para as mutations criadas e também executar as mutações de incrementar e decrementar o estado count

actions module-example

Observe que simulamos operações assíncronas em nossas actions adicionando um pequeno delay com setTimeout. Substituímos as mutations do Index.vue pelas actions e modificamos nossos botões para dispará-las. Observe que usamos agora os mapActions para facilitar:

Index.vue com actons

Agora, ao clicar nos botões de incremento e decremento, leva-se um segundo para que o estado count sofra a mutação.

Mas onde está o gerenciamento de estado global?

Por enquanto, apenas vimos a mudança de estado em uma única view. Porém, vamos fazer agora um exemplo para que possamos ver mais claramente como todo esse gerenciamento é feito separadamente do estado local dos componentes Vue.

Primeiro vamos criar um componente vue, com outros dois botões de incremento e decremento. No diretório components criamos o arquivo CButtons.vue.

Nestes componentes vamos usar a mesma lógica de incremento e decremento que fizemos no Index.vue com o mapeamento das actions:

Componente CButtons

Na page Index.vue importamos nosso componente e registramos ele:

Index.vue com o componente CButtons

O resultado agora é nossa page Index.vue com quatro botões, dois que estão no HTML do próprio Index.vue, e os outros dois do componente importado CButtons.

Normalmente, para que fosse possível a troca de informações entre componentes pai/filho, era necessário props e events. Porém, com o Vuex podemos ter acesso à store em qualquer parte da nossa aplicação.

Aplicação final

Finalizando

Este artigo é uma breve adaptação da documentação oficial do Vuex, com uma melhor organização e exemplos. Claro que a documentação possui muito mais conteúdo, e aconselho a você que leia com calma e pratique os exemplos.

Deixo aqui alguns links interessantes para você evoluir ainda mais com o Vuex: