Um mergulho profundo nas vantagens e recursos do NgRx

Publicados: 2022-03-11

Se um líder de equipe instruir um desenvolvedor a escrever muito código clichê em vez de escrever alguns métodos para resolver um determinado problema, eles precisam de argumentos convincentes. Engenheiros de software são solucionadores de problemas; eles preferem automatizar as coisas e evitar clichês desnecessários.

Embora o NgRx venha com algum código clichê, ele também fornece ferramentas poderosas para desenvolvimento. Este artigo demonstra que gastar um pouco mais de tempo escrevendo código trará benefícios que fazem valer a pena o esforço.

A maioria dos desenvolvedores começou a usar o gerenciamento de estado quando Dan Abramov lançou a biblioteca Redux. Alguns começaram a usar a gestão estatal porque era uma tendência, não porque lhes faltava. Os desenvolvedores que usam um projeto padrão “Hello World” para gerenciamento de estado podem rapidamente se encontrar escrevendo o mesmo código repetidamente, aumentando a complexidade sem nenhum ganho.

Eventualmente, alguns ficaram frustrados e abandonaram completamente a gestão estatal.

Meu problema inicial com o NgRx

Acho que essa preocupação clichê foi um grande problema com o NgRx. No início, não conseguimos ver o quadro geral por trás disso. NgRx é uma biblioteca, não um paradigma de programação ou uma mentalidade. No entanto, para entender completamente a funcionalidade e usabilidade dessa biblioteca, precisamos expandir um pouco mais nosso conhecimento e focar na programação funcional. É aí que você pode escrever código clichê e se sentir feliz com isso. (Estou falando sério.) Eu já fui um cético do NgRx; agora sou um admirador do NgRx.

Há algum tempo, comecei a usar o gerenciamento de estado. Passei pela experiência padrão descrita acima, então decidi parar de usar a biblioteca. Como adoro JavaScript, tento obter pelo menos um conhecimento fundamental sobre todos os frameworks populares em uso hoje. Aqui está o que eu aprendi usando o React.

O React tem um recurso chamado Hooks. Assim como os Componentes em Angular, Hooks são funções simples que aceitam argumentos e retornam valores. Um gancho pode ter um estado nele, que é chamado de efeito colateral. Então, por exemplo, um simples botão em Angular pode ser traduzido para React assim:

 @Component({ selector: 'simple-button', template: ` <button>Hello {{ name }}</button> `, }) export class SimpleButtonComponent { @Input() name!: string; } export default function SimpleButton(props: { name: string }) { return <button>{props.name} </button>; }

Como você pode ver, esta é uma transformação direta:

  • SimpleButtonComponent => SimpleButton
  • @Input() nome => props.name
  • modelo => valor de retorno

Ilustração: Angular Component e React Hooks são bastante semelhantes.
Angular Component e React Hooks são bastante semelhantes.

Nossa função React SimpleButton tem uma característica importante no mundo da programação funcional: é uma função pura . Se você está lendo isso, presumo que você já ouviu esse termo pelo menos uma vez. O NgRx.io cita funções puras duas vezes nos Conceitos-chave:

  • As alterações de estado são tratadas por funções puras chamadas reducers que usam o estado atual e a última ação para calcular um novo estado.
  • Seletores são funções puras usadas para selecionar, derivar e compor pedaços de estado.

No React, os desenvolvedores são encorajados a usar Hooks como funções puras o máximo possível. Angular também incentiva os desenvolvedores a implementar o mesmo padrão usando o paradigma Smart-Dumb Component.

Foi quando percebi que me faltavam algumas habilidades cruciais de programação funcional. Não demorou muito para entender o NgRx, pois depois de aprender os conceitos-chave da programação funcional, tive um “Aha! momento”: melhorei minha compreensão do NgRx e queria usá-lo mais para entender melhor os benefícios que ele oferece.

Este artigo compartilha minha experiência de aprendizado e o conhecimento que adquiri sobre NgRx e programação funcional. Não explico a API para NgRx ou como chamar ações ou usar seletores. Em vez disso, compartilho por que passei a apreciar que o NgRx é uma ótima biblioteca: não é apenas uma tendência relativamente nova, ele oferece uma série de benefícios.

Vamos começar com programação funcional .

Programação Funcional

A programação funcional é um paradigma que difere muito de outros paradigmas. Este é um tema muito complexo com muitas definições e diretrizes. No entanto, a programação funcional contém alguns conceitos básicos e conhecê-los é um pré-requisito para dominar o NgRx (e JavaScript em geral).

Esses conceitos básicos são:

  • Função pura
  • Estado imutável
  • Efeito colateral

Repito: é apenas um paradigma, nada mais. Não há nenhuma biblioteca funcional.js que baixamos e usamos para escrever software funcional. É apenas uma maneira de pensar sobre como escrever aplicativos. Vamos começar com o conceito central mais importante: função pura .

Função pura

Uma função é considerada uma função pura se seguir duas regras simples:

  • Passar os mesmos argumentos sempre retorna o mesmo valor
  • A falta de um efeito colateral observável envolvido na execução da função (uma mudança de estado externa, chamada de operação de E/S, etc.)

Portanto, uma função pura é apenas uma função transparente que aceita alguns argumentos (ou nenhum argumento) e retorna um valor esperado. Você tem certeza de que chamar essa função não resultará em efeitos colaterais, como rede ou alteração de algum estado global do usuário.

Vejamos três exemplos simples:

 //Pure function function add(a,b){ return a + b; } //Impure function breaking rule 1 function random(){ return Math.random(); } //Impure function breaking rule 2 function sayHello(name){ console.log("Hello " + name); }
  • A primeira função é pura porque sempre retornará a mesma resposta ao passar os mesmos argumentos.
  • A segunda função não é pura porque não é determinística e retorna respostas diferentes toda vez que é chamada.
  • A terceira função não é pura porque usa um efeito colateral (chamando console.log ).

É fácil discernir se a função é pura ou não. Por que uma função pura é melhor que uma impura? Porque é mais simples de pensar. Imagine que você está lendo algum código-fonte e vê uma chamada de função que você sabe que é pura. Se o nome da função estiver correto, você não precisa explorá-lo; você sabe que não muda nada, ele retorna o que você espera. É crucial para a depuração quando você tem um aplicativo corporativo enorme com muita lógica de negócios, pois pode economizar muito tempo.

Além disso, é simples de testar. Você não precisa injetar nada ou simular algumas funções dentro dele, basta passar argumentos e testar se o resultado é compatível. Há uma forte conexão entre o teste e a lógica: se um componente é fácil de testar, é fácil entender como e por que ele funciona.

As funções puras vêm com uma funcionalidade muito útil e amigável ao desempenho chamada memoization. Se soubermos que chamar os mesmos argumentos retornará o mesmo valor, podemos simplesmente armazenar em cache os resultados e não perder tempo chamando-os novamente. NgRx definitivamente fica no topo da memoização; essa é uma das principais razões pelas quais é rápido.

A transformação deve ser intuitiva e transparente.

Você pode se perguntar: “E os efeitos colaterais? Onde eles vão?" Em sua palestra GOTO, Russ Olsen brinca que nossos clientes não nos pagam por funções puras, eles nos pagam por efeitos colaterais. Isso é verdade: ninguém se importa com a função pura da Calculadora se ela não for impressa em algum lugar. Os efeitos colaterais têm seu lugar no universo da programação funcional. Veremos isso em breve.

Por enquanto, vamos passar para a próxima etapa na manutenção de arquiteturas de aplicativos complexos, o próximo conceito central: estado imutável .

Estado imutável

Existe uma definição simples para um estado imutável:

  • Você só pode criar ou excluir um estado. Você não pode atualizá-lo.

Em termos simples, para atualizar a idade de um objeto User … :

 let user = { username:"admin", age:28 }

… você deve escrever assim:

 // Not like this newUser.age = 30; // But like this let newUser = {...user, age:29 }

Cada mudança é um novo objeto que copiou propriedades dos antigos. Como tal, já estamos em uma forma de estado imutável.

String, Boolean e Number são estados imutáveis: você não pode acrescentar ou modificar valores existentes. Em contraste, uma Data é um objeto mutável: você sempre manipula o mesmo objeto de data.

A imutabilidade se aplica em toda a aplicação: Se você passar um objeto de usuário dentro da função que altera sua idade, ele não deve alterar um objeto de usuário, deve criar um novo objeto de usuário com idade atualizada e devolvê-lo:

 function updateAge(user, age) { return {...user, age: age) } let user = {username: 'admin', age: 29}; let newUser = updateAge(user, 32);

Por que devemos dedicar tempo e atenção a isso? Há alguns benefícios que merecem destaque.

Um benefício para linguagens de programação de back-end envolve o processamento paralelo. Se uma mudança de estado não depende de uma referência e cada atualização é um novo objeto, você pode dividir o processo em partes e trabalhar na mesma tarefa com inúmeras threads sem compartilhar a mesma memória. Você pode até mesmo paralelizar tarefas entre servidores.

Para estruturas como Angular e React, o processamento paralelo é uma das maneiras mais benéficas de melhorar o desempenho do aplicativo. Por exemplo, o Angular precisa verificar as propriedades de cada objeto que você passa por meio de associações de entrada para discernir se um componente deve ser renderizado ou não. Mas se ChangeDetectionStrategy.OnPush em vez do padrão, ele verificará por referência e não por cada propriedade. Em um aplicativo grande, isso definitivamente economiza tempo. Se atualizarmos nosso estado de forma imutável, obteremos esse aumento de desempenho gratuitamente.

O outro benefício para um estado imutável que todas as linguagens de programação e frameworks compartilham é semelhante aos benefícios das funções puras: é mais fácil pensar e testar. Quando uma mudança é um novo estado nascido de um antigo, você sabe exatamente no que está trabalhando e pode rastrear exatamente como e onde o estado mudou. Você não perde o histórico de atualizações e pode desfazer/refazer alterações para o estado (React DevTools é um exemplo).

No entanto, se um único estado for atualizado, você não conhecerá o histórico dessas alterações. Pense em um estado imutável como o histórico de transações de uma conta bancária. É praticamente um must-have.

Agora que analisamos a imutabilidade e a pureza, vamos abordar o conceito central restante: efeito colateral .

Efeito colateral

Podemos generalizar a definição de efeito colateral:

  • Na ciência da computação, diz-se que uma operação, função ou expressão tem um efeito colateral se modificar algum valor de variável de estado fora de seu ambiente local. Ou seja, tem um efeito observável além de retornar um valor (o efeito principal) ao invocador da operação.

Simplificando, tudo que altera um estado fora do escopo da função – todas as operações de E/S e algum trabalho que não está diretamente conectado à função – pode ser considerado um efeito colateral. No entanto, temos que evitar o uso de efeitos colaterais dentro de funções puras porque os efeitos colaterais contradizem a filosofia de programação funcional. Se você usar uma operação de E/S dentro de uma função pura, ela deixará de ser uma função pura.

No entanto, precisamos ter efeitos colaterais em algum lugar, pois um aplicativo sem eles seria inútil. Em Angular, não apenas as funções puras precisam ser protegidas contra efeitos colaterais, mas também devemos evitar usá-las em Componentes e Diretivas.

Vamos examinar como podemos implementar a beleza dessa técnica dentro do framework Angular.

Ilustração: Efeito Colateral Angular NgRx

Programação Angular Funcional

Uma das primeiras coisas a entender sobre o Angular é a necessidade de desacoplar componentes em componentes menores com a maior frequência possível para facilitar a manutenção e os testes. Isso é necessário, pois precisamos dividir nossa lógica de negócios. Além disso, os desenvolvedores Angular são incentivados a deixar componentes apenas para fins de renderização e mover toda a lógica de negócios dentro dos serviços.

Para expandir esses conceitos, os usuários do Angular adicionaram o padrão “Dumb-Smart Component” ao seu vocabulário. Esse padrão requer que as chamadas de serviço não existam dentro dos componentes pequenos. Como a lógica de negócios reside nos serviços, ainda precisamos chamar esses métodos de serviço, aguardar sua resposta e só então fazer as alterações de estado. Assim, os componentes têm alguma lógica comportamental dentro deles.

Para evitar isso, podemos criar um Componente Inteligente (Componente Raiz), que contém lógica de negócios e comportamento, passar os estados via Propriedades de Entrada e chamar ações ouvindo os parâmetros de Saída. Dessa forma, os pequenos componentes são realmente apenas para fins de renderização. É claro que nosso Componente Raiz precisa ter algumas chamadas de serviço dentro dele e não podemos simplesmente removê-las, mas sua utilidade seria limitada apenas à lógica de negócios, não à renderização.

Vejamos um exemplo de Componente Contador. Um contador é um componente que possui dois botões que aumentam ou diminuem o valor e um displayField que exibe o currentValue . Assim terminamos com quatro componentes:

  • CounterContainer
  • Botão Aumentar
  • Botão Diminuir
  • Valor atual

Toda a lógica vive dentro do CounterContainer , então todos os três são apenas renderizadores. Aqui está o código para os três:

 @Component({ selector: 'decrease-button', template: `<button (click)="increase.emit()" [disabled]="disabled"> Decrease </button>`, }) export class DecreaseButtonComponent { @Input() disabled!: boolean; @Output() increase = new EventEmitter(); } @Component({ selector: 'current-value', template: `<button> {{ currentValue }} </button>`, }) export class CurrentValueComponent { @Input() currentValue!: string; } @Component({ selector: 'increase-button', template: `<button (click)="increase.emit()" [disabled]="disabled"> Increase </button>`, }) export class IncreaseButtonComponent { @Input() disabled!: boolean; @Output() increase = new EventEmitter(); }

Veja como são simples e puros. Eles não têm estado ou efeitos colaterais, apenas dependem de propriedades de entrada e eventos de emissão. Imagine como é fácil testá-los. Podemos chamá-los de componentes puros, pois é isso que eles realmente são. Eles dependem apenas de parâmetros de entrada, não têm efeitos colaterais e sempre retornam o mesmo valor (string de modelo) passando os mesmos parâmetros.

Assim, funções puras na programação funcional são transferidas para os componentes puros em Angular. Mas para onde vai toda a lógica? A lógica ainda está lá, mas em um lugar um pouco diferente, ou seja, o CounterComponent .

 @Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="decreaseIsDisabled" (decrease)="decrease()"> </decrease-button> <current-value [currentValue]="currentValue"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled"> </increase-button> `, }) export class CounterContainerComponent implements OnInit { @Input() disabled!: boolean; currentValue = 0; get decreaseIsDisabled() { return this.currentValue === 0; } get increaseIsDisabled() { return this.currentValue === 100; } constructor() {} ngOnInit(): void {} decrease() { this.currentValue -= 1; } increase() { this.currentValue += 1; } }

Como você pode ver, a lógica de comportamento vive no CounterContainer , mas a parte de renderização está ausente (ela declara componentes dentro do modelo) porque a parte de renderização é para componentes puros.

Poderíamos injetar tanto serviço quanto quisermos porque lidamos com todas as manipulações de dados e mudanças de estado aqui. Uma coisa que vale a pena mencionar é que, se tivermos um componente aninhado profundo, não devemos criar apenas um componente de nível raiz. Podemos dividi-lo em componentes inteligentes menores e usar o mesmo padrão. Em última análise, depende da complexidade e do nível aninhado de cada componente.

Podemos pular facilmente desse padrão para a própria biblioteca NgRx, que fica apenas uma camada acima dela.

Biblioteca NgRx

Podemos dividir qualquer aplicação web em três partes principais:

  • Logíca de negócios
  • Estado do aplicativo
  • Lógica de renderização

Ilustração da lógica de negócios, estado do aplicativo e lógica de renderização.

Business Logic é todo o comportamento que está acontecendo com a aplicação, como rede, entrada, saída, API, etc.

Estado do aplicativo é o estado do aplicativo. Pode ser global, como o usuário atualmente autorizado, e também local, como o valor atual do Componente do Contador.

A lógica de renderização abrange a renderização, como exibir dados usando DOM, criar ou remover elementos e assim por diante.

Ao usar o padrão Dumb-Smart, desacoplamos a Lógica de Renderização da Lógica de Negócios e do Estado do Aplicativo, mas também podemos dividi-los porque ambos são conceitualmente diferentes um do outro. O estado do aplicativo é como um instantâneo do seu aplicativo no tempo atual. A lógica de negócios é como uma funcionalidade estática que está sempre presente em seu aplicativo. A razão mais importante para dividi-los é que a lógica de negócios é principalmente um efeito colateral que queremos evitar o máximo possível no código do aplicativo. É quando a biblioteca NgRx, com seu paradigma funcional, brilha.

Com o NgRx você dissocia todas essas partes. Existem três partes principais:

  • Redutores
  • Ações
  • Seletores

Combinados com programação funcional, todos os três se combinam para nos dar uma ferramenta poderosa para lidar com aplicativos de qualquer tamanho. Vamos examinar cada um deles.

Redutores

Um redutor é uma função pura, que possui uma assinatura simples. Ele recebe um estado antigo como parâmetro e retorna um novo estado, derivado do antigo ou de um novo. O próprio estado é um único objeto, que vive com o ciclo de vida do seu aplicativo. É como uma tag HTML, um único objeto raiz.

Você não pode modificar diretamente um objeto de estado, você precisa modificá-lo com redutores. Isso tem uma série de benefícios:

  • A lógica de mudança de estado vive em um único lugar, e você sabe onde e como o estado muda.
  • As funções do redutor são funções puras, fáceis de testar e gerenciar.
  • Como os redutores são funções puras, eles podem ser memorizados, tornando possível armazená-los em cache e evitar computação extra.
  • As mudanças de estado são imutáveis. Você nunca atualiza a mesma instância. Em vez disso, você sempre retorna um novo. Isso permite uma experiência de depuração de “viagem no tempo”.

Este é um exemplo trivial de um redutor:

 function usernameReducer(oldState, username) { return {...oldState, username} }

Mesmo sendo um redutor fictício muito simples, é o esqueleto para todos os redutores longos e complexos. Todos compartilham os mesmos benefícios. Poderíamos ter centenas de redutores em nossa aplicação e podemos fazer quantos quisermos.

Para nosso Componente Contador, nosso estado e redutores podem ficar assim:

 interface State{ decreaseDisabled:boolean; increaseDisabled:boolean; currentValue:number; } const MIN_VALUE=0; const MAX_VALUE =100; function decreaseReducer(oldState) { const newValue = oldState.currentValue -1 return {...oldState,currentValue : newValue, decreaseDisabled: newValue===MIN_VALUE } function increaseReducer(oldState) { const newValue = oldState.currentValue + 1 return {...oldState,currentValue : newValue, decreaseDisabled: newValue===MAX_VALUE }

Removemos o estado do componente. Agora precisamos de uma maneira de atualizar nosso estado e chamar o redutor apropriado. É quando as ações entram em jogo.

Ações

Uma ação é uma maneira de notificar o NgRx para chamar um redutor e atualizar o estado. Sem isso, não faria sentido usar o NgRx. Uma ação é um objeto simples que anexamos ao redutor atual. Após chamá-lo, o redutor apropriado será chamado, então em nosso exemplo poderíamos ter as seguintes ações:

 enum CounterActions { IncreaseValue = '[Counter Component] Increase Value', DecreaseValue = '[Counter Component] Decrease Value', } on(CounterActions.IncreaseValue,increaseReducer); on(CounterActions.DecreaseValue,decreaseReducer);

Nossas ações estão atreladas aos redutores. Agora podemos modificar ainda mais nosso Componente de Contêiner e chamar as ações apropriadas quando necessário:

 @Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="decreaseIsDisabled" (decrease)="decrease()"> </decrease-button> <current-value [currentValue]="currentValue"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled"> </increase-button> `, }) export class CounterContainerComponent implements OnInit { constructor(private store: Store<any>) {} decrease() { this.store.dispatch(CounterActions.DicreaseValue); } increase() { this.store.dispatch(CounterActions.IncreaseValue); } }

Observação: removemos o estado e adicionaremos novamente em breve .

Agora nosso CounterContainer não tem lógica de mudança de estado. Ele só sabe o que despachar. Agora precisamos de alguma maneira de exibir esses dados para a exibição. Essa é a utilidade dos seletores.

Seletores

Um seletor também é uma função pura muito simples, mas diferente de um redutor, ele não atualiza o estado. Como o nome indica, o seletor apenas o seleciona. Em nosso exemplo, poderíamos ter três seletores simples:

 function selectCurrentValue(state) { return state.currentValue; } function selectDicreaseIsDisabled(state) { return state.decreaseDisabled; } function selectIncreaseIsDisabled(state) { return state.increaseDisabled; }

Usando esses seletores, poderíamos selecionar cada fatia de um estado dentro de nosso componente CounterContainer inteligente.

 @Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="ecreaseIsDisabled$ | async" (decrease)="decrease()" > </decrease-button> <current-value [currentValue]="currentValue$ | async"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled$ | async" > </increase-button> `, }) export class CounterContainerComponent implements OnInit { decreaseIsDisabled$ = this.store.select(selectDicreaseIsDisabled); increaseIsDisabled$ = this.store.select(selectIncreaseIsDisabled); currentValue$ = this.store.select(selectCurrentValue); constructor(private store: Store<any>) {} decrease() { this.store.dispatch(CounterActions.DicreaseValue); } increase() { this.store.dispatch(CounterActions.IncreaseValue); } }

Essas seleções são assíncronas por padrão (assim como os Observables em geral). Isso não tem importância, pelo menos do ponto de vista do padrão. O mesmo seria verdade para um síncrono, pois apenas selecionaríamos algo do nosso estado.

Vamos dar um passo atrás e olhar para o quadro geral para ver o que alcançamos até agora. Temos um Aplicativo de Contador, que possui três partes principais que são quase desacopladas umas das outras. Ninguém sabe como o estado do aplicativo está gerenciando a si mesmo ou como a camada de renderização renderiza o estado.

As partes desacopladas usam a ponte (Ações, Seletores) para se conectarem umas às outras. Eles são desacoplados de tal forma que poderíamos pegar todo o código do State Application e movê-lo para outro projeto, como para uma versão móvel, por exemplo. A única coisa que teríamos que implementar seria a renderização. Mas e os testes?

Na minha humilde opinião, testar é a melhor parte do NgRx. Testar este projeto de amostra é semelhante a jogar jogo da velha. Existem apenas funções puras e componentes puros, então testá-los é muito fácil. Agora imagine se esse projeto se tornar maior, com centenas de componentes. Se seguirmos o mesmo padrão, apenas adicionaríamos mais e mais peças. Não se tornaria uma bolha confusa e ilegível de código-fonte.

Estamos quase terminando. Há apenas uma coisa importante para cobrir: efeitos colaterais. Mencionei efeitos colaterais muitas vezes até agora, mas parei de explicar onde armazená-los.

Isso porque os efeitos colaterais são a cereja do bolo e, ao construir esse padrão, é muito fácil removê-los do código do aplicativo.

Efeitos colaterais

Digamos que nosso aplicativo de contador tenha um cronômetro e a cada três segundos ele aumente automaticamente o valor em um. Este é um efeito colateral simples, que tem que viver em algum lugar. É o mesmo efeito colateral, por definição, de uma solicitação Ajax.

Se pensarmos nos efeitos colaterais, a maioria tem duas razões principais para existir:

  • Fazer qualquer coisa fora do ambiente do estado
  • Atualizando o estado do aplicativo

Por exemplo, armazenar algum estado dentro do LocalStorage é a primeira opção, enquanto atualizar o estado da resposta do Ajax é a segunda. Mas ambos compartilham a mesma assinatura: cada efeito colateral tem que ter algum ponto de partida. Ele precisa ser chamado pelo menos uma vez para solicitar que inicie a ação.

Como descrevemos anteriormente, o NgRx tem uma boa ferramenta para dar um comando a alguém. Isso é uma ação. Poderíamos chamar qualquer efeito colateral despachando uma ação. O pseudocódigo pode ficar assim:

 function startTimer(){ setInterval(()=>{ console.log("Hello application"); },3000) } on(CounterActions.StartTime,startTimer) ... // We start timer by dispatching an action dispatch(CounterActions.StartTime);

É bem trivial. Como mencionei anteriormente, os efeitos colaterais atualizam algo ou não. Se um efeito colateral não atualizar nada, não há nada a fazer; nós apenas deixamos. Mas se queremos atualizar um estado, como fazemos isso? Da mesma forma que um componente tenta atualizar um estado: chamando outra ação. Então chamamos uma ação dentro do efeito colateral, que atualiza o estado:

 function startTimer(store) { setInterval(()=> { // We are dispatching another action dispatch(CounterActions.IncreaseValue) }, 3000) } on(CounterActions.StartTime, startTimer); ... // We start timer by dispatching an action dispatch(CounterActions.StartTime);

Agora temos um aplicativo totalmente funcional.

Resumindo nossa experiência com NgRx

Há alguns tópicos importantes que gostaria de mencionar antes de terminarmos nossa jornada NgRx:

  • O código mostrado é um pseudo código simples que inventei para o artigo; ele é adequado apenas para fins de demonstração. NgRx é o lugar onde vivem as fontes reais.
  • Não há nenhuma diretriz oficial que comprove minha teoria sobre conectar programação funcional com a biblioteca NgRx. É apenas minha opinião formada depois de ler dezenas de artigos e exemplos de código-fonte criados por pessoas altamente qualificadas.
  • Depois de usar o NgRx, você definitivamente perceberá que é muito mais complexo do que este simples exemplo. Meu objetivo não era fazer parecer mais simples do que realmente é, mas mostrar que mesmo sendo um pouco complexo e pode até resultar em um caminho mais longo até o destino, vale a pena o esforço adicional.
  • O pior uso do NgRx é usá-lo em qualquer lugar, independentemente do tamanho ou complexidade do aplicativo. Existem alguns casos em que você não deve usar o NgRx; por exemplo, em formulários. É quase impossível implementar formulários dentro do NgRx. Os formulários são colados no próprio DOM; eles não podem viver separadamente. Se você tentar dissociá-los, acabará odiando não apenas o NgRx, mas a tecnologia da web em geral.
  • Às vezes, usar o mesmo código clichê, mesmo para um pequeno exemplo, pode se tornar um pesadelo, mesmo que possa nos beneficiar no futuro. Se for esse o caso, basta integrar com outra biblioteca incrível, que faz parte do ecossistema NgRx (ComponentStore).