Conteúdos
  1. 1. Estado local intermediário
  2. 2. Estado compartilhado persistente
  3. 3. Comunicação entre componentes

No post passado da série Angular por um desenvolvedor React introduzi o conceito de componentes a serem usados no Angular bastante semelhante com os padrões usados em aplicações desenvolvidas com React.

Neste segundo post vou falar um pouco sobre o fluxo de dados, o modo como a mudança do estado da aplicação pode ser propagada para todos os componentes e um pouco da minha opinião sobre o two-way data binding e como ele deve ser usado.

Assim como com o React, eu gosto bastante da arquitetura Flux e a ideia de ter um fluxo único de dados. Ela é uma arquitetura que pode ser aplicada independente de framework ou linguagem. Cheguei inclusive à fazer alguns experimentos usando arquitetura flux em um app jQuery, e não houve nada que achei ser impossível sem estar usando o React. (Caso veja o código do app com jQuery, pode ser que encontre algumas coisas feias no código, eu não cheguei a me dedicar tanto nele, dado que foi apenas um experimento).

Em meus estudos sobre Angular tendo React em mente, comecei a pensar em modos de criar apps que não fiquem pouco manuteníveis ou difíceis de testar por conta do two-way data binding.

Caso você não tenha familiaridade com o termo, two-day data binding é uma funcionalidade de um sistema que permite que duas unidades que compartilhem dados entre si possam modificar uma a outra. Em se tratando de MVC, seria um caso onde modificações na view são refletidas no model, e modificações no model são refletidas na view, algo bem comum em aplicações feitas com Angular.

Mas qual o problema nisso? Em geral, nenhum, mas as pessoas costumam abusar desta funcionalidade sem notar que isto pode ser nocivo. Digamos que você tem um componente B dentro de um componente A. Você passa um dado x de A para B, e a view de B possui two-way data binding para modificar x. Isso será refletido em A também, e isso criará um acoplamento entre estes dois componentes. Acho que não preciso me aprofundar muito no caso para que você note o problema em código acoplado, certo?

E como resolver este problema? E, sendo mais específico, como resolver este problema em um app Angular? Eu acredito que a solução seja usar algo semelhante à arquitetura flux, utilizando três conceitos: a) estado local intermediário, b) estado compartilhado persistente, e c) comunicação entre componentes.

Estado local intermediário

O primeiro passo é notar que estado local é algo não totalmente evitável. É bom, e saudável, evitar o máximo de estado local possível. O pessoal que usa o React que está lendo este post vai entender bem ao que me refiro. Enquanto o React tem built-in um sistema para gerenciamento de estado interno de um componente (vide setState, o próprio Facebook sugere que você evite ao máximo o uso de armazenamento de estado dentro de um componente e favoreça o uso de componentes burros).

Porém as vezes é necessário armazenar um estado intermediário localmente. Quer um exemplo? Um componente de accordion menu onde mais de um menu pode estar aberto ao mesmo tempo. Em uma aplicação flux você pode muito bem manter os dados de qual menu está aberto dentro de uma store, mas note que isso não é dado do sistema, não é necessariamente parte do estado do sistema, e pode ser armazenado localmente dentro do componente. Porém, desde que os dados sobre quais menus estão abertos não precisem ser compartilhados com outra parte do sistema, ou não sejam relevantes para o estado geral do sistema. Um outro exemplo, um pouco mais prático: dados de um formulário. Eu acredito que dados que serão enviados para o backend e processados devam ser mantidos locais internamente ao formulário até o momento de envio para o backend, sem armazenar os dados na(s) store(s).

Sinceramente, eu acredito que este tipo de dado deva ser o único usado com two-way data binding. Entende o que eu digo? Já que é um dado local, fazer este binding de dados não vai vazar para o resto do sistema. Se, após algumas alterações locais, este dado precisar ser compartilhado com o resto do sistema e/ou persistido, ele será transformado em um estado compartilhado persistente (como é o caso dos formulários citado acima).

Estado compartilhado persistente

Se você já desenvolveu ou mexeu com uma aplicação flux, saiba que este tipo de estado pode ser entendido como: dados que são salvos em uma store. Dados que fazem parte do estado do sistema.

Por exemplo, digamos que você tem um campo onde o usuário pode editar o texto do mesmo à vontade, que nada mais se alterará no sistema, porém quando ele apertar enter, o conteúdo do campo deva ser exibido numa mensagem de boas vindas ao usuário no topo da página.

No momento em que este dado é “publicado” para o resto da aplicação, ele deixa de ser um dado local temporário, e passa a ser um dado compartilhado persistente. De maneira análoga, ele deixa de estar dentro do state de um componente do React e passa a ser um dados que vai chegar ao componente em forma de prop.

Mas como avisar o resto da aplicação sobre esta mudança? Bom, agora entra a parte mais importante do post: comunicação entre componentes.

Comunicação entre componentes

A comunicação entre os diferentes componentes da sua aplicação é o que dita o fluxo de dados da mesma. Ter vários two-way data bindings espalhados de maneira desorganizada no seu sistema tornará ele pouco manutenível e será pouco debugável. Isso fará com que analizar erros na sua aplicação seja bem mais difícil.

Se você já tem experiência com React e flux, já deve ter pensado: poxa, isso pode ser resolvido por um dispatcher. E você está certo! Mas tem um detalhe.

Usar um simples dispatcher ou event emitter no seu sistema Angular, apesar de funcionar, não é a melhor opção por causa do $digest loop do Angular.

O digest loop é a ordem em que as checagens, alterações, watches e renderizações são feitas automaticamente pelo Angular. É um ciclo que deve ser respeitado para manter o bom funcionamente e, principalmente, previsibilidade da sua aplicação. Se você já mexeu um pouco com Angular já deve ter se deparado com algum erro mais ou menos assim:

Error: $digest already in progress

Isso significa que o ciclo de $digest já estava em progresso e algo tentou iniciá-lo novamente. Acredite, este é um problema bem chato de identificar a causa as vezes, e que pode acabar acontecendo se você usar um event emitter comum na sua aplicação. Mas como usar algo parecido com um dispatcher e que respeite o $digest? A resposta é: usar o $rootScope!

Toda aplicação Angular tem um escopo que é pai de todos os outros escopos, ele é chamado de $rootScope. Assim como qualquer outro escopo do Angular, ele possui os métodos $on e $emit, que é tudo que precisamos para construir um event emitter simples, rápido, e que respeita o $digest. Vamos lá? Veja um exemplo usando ES6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const eventEmitterService = [
'$rootScope',
($rootScope) => {
return {
on: (...args) => {
$rootScope.$on(...args);
},
emit: (...args) => {
$rootScope.$emit(...args);
}
}
}
];
app.factory('eventEmitter', eventEmitterService);

Pronto! Simples, não? Pois é, agora você você consegue compartilhar os dados entre seus componentes (e factories, e services e tudo mais) de uma maneira organizada e permitir que outras partes do seu sistema possam esperar pela mudança. Sacou aqui a semelhança com o esquema de usar actions e stores do flux?

Agora, no nosso exemplo ali do campo de texto, você pode ter um componente que tem um estado interno do conteúdo do campo e, quando o usuário pressionar enter, você usar o nosso event emitter para emitir o evento TEXT_CHANGED (ou algo do tipo) com o novo texto.

E isto não precisa ser aplicado somente a textos. Em experiências recentes minhas com o Angular, eu fiz com que o controller da página de um post de blog ouvisse as modificações nos comentários daquele post. Assim, quando o usuário cria um novo comentário, ele é persistido no servidor e, quando a resposta de confirmação do servidor chega, eu emito um evento ADD_COMMENT que é ouvido pelo controller da página e ele atualiza a sessão de comentários sem ter que recarregar a página.

Você pode ver um exemplo de como usar este approach na tag de data-flow do nosso repositório de apoio.

Espero que tenham gostado, até a próxima!

Conteúdos
  1. 1. Estado local intermediário
  2. 2. Estado compartilhado persistente
  3. 3. Comunicação entre componentes