Tutorial Ngrx e Angular 2: Construindo um Aplicativo Reativo

Publicados: 2022-03-11

Falamos muito sobre programação reativa no reino Angular. A programação reativa e o Angular 2 parecem andar de mãos dadas. No entanto, para quem não está familiarizado com as duas tecnologias, pode ser uma tarefa bastante difícil descobrir do que se trata.

Neste artigo, através da construção de um aplicativo Angular 2 reativo usando Ngrx, você aprenderá o que é o padrão, onde o padrão pode ser útil e como o padrão pode ser usado para criar aplicativos Angular 2 melhores.

Ngrx é um grupo de bibliotecas Angular para extensões reativas. O Ngrx/Store implementa o padrão Redux usando os conhecidos observáveis ​​RxJS do Angular 2. Ele oferece várias vantagens ao simplificar o estado do aplicativo para objetos simples, reforçar o fluxo de dados unidirecional e muito mais. A biblioteca Ngrx/Effects permite que o aplicativo se comunique com o mundo exterior acionando efeitos colaterais.

O que é programação reativa?

Programação reativa é um termo que você ouve muito hoje em dia, mas o que isso realmente significa?

A programação reativa é uma maneira de os aplicativos manipularem eventos e fluxo de dados em seus aplicativos. Na programação reativa, você projeta seus componentes e outras partes de seu software para reagir a essas mudanças em vez de solicitar mudanças. Esta pode ser uma grande mudança.

Uma ótima ferramenta para programação reativa, como você deve saber, é o RxJS.

Ao fornecer observáveis ​​e muitos operadores para transformar os dados recebidos, essa biblioteca o ajudará a lidar com eventos em seu aplicativo. Na verdade, com observáveis, você pode ver o evento como um fluxo de eventos e não como um evento único. Isso permite combiná-los, por exemplo, para criar um novo evento que você ouvirá.

A programação reativa é uma mudança na maneira como você se comunica entre as diferentes partes de um aplicativo. Em vez de enviar dados diretamente para o componente ou serviço que precisava deles, na programação reativa, é o componente ou serviço que reage às alterações de dados.

Uma palavra sobre Ngrx

Para entender o aplicativo que você construirá por meio deste tutorial, você deve mergulhar rapidamente nos principais conceitos do Redux.

Armazenar

A loja pode ser vista como seu banco de dados do lado do cliente, mas, mais importante, reflete o estado do seu aplicativo. Você pode vê-lo como a única fonte da verdade.

É a única coisa que você altera quando segue o padrão Redux e modifica enviando ações para ele.

Redutor

Redutores são as funções que sabem o que fazer com uma determinada ação e o estado anterior do seu aplicativo.

Os redutores pegarão o estado anterior de sua loja e aplicarão uma função pura a ele. Pure significa que a função sempre retorna o mesmo valor para a mesma entrada e que não tem efeitos colaterais. A partir do resultado dessa função pura, você terá um novo estado que será colocado em sua loja.

Ações

As ações são a carga útil que contém as informações necessárias para alterar sua loja. Basicamente, uma ação tem um tipo e uma carga útil que sua função redutora levará para alterar o estado.

Expedidor

Despachantes são simplesmente um ponto de entrada para você despachar sua ação. No Ngrx, existe um método de despacho diretamente na loja.

Middleware

Middleware são algumas funções que irão interceptar cada ação que está sendo despachada para criar efeitos colaterais, mesmo que você não as use neste artigo. Eles são implementados na biblioteca Ngrx/Effect e há uma grande chance de você precisar deles ao construir aplicativos do mundo real.

Por que usar Ngrx?

Complexidade

O armazenamento e o fluxo de dados unidirecional reduzem bastante o acoplamento entre as partes do seu aplicativo. Esse acoplamento reduzido reduz a complexidade de sua aplicação, pois cada parte se preocupa apenas com estados específicos.

Ferramentas

Todo o estado do seu aplicativo é armazenado em um só lugar, por isso é fácil ter uma visão global do estado do seu aplicativo e ajuda durante o desenvolvimento. Além disso, com o Redux vem muitas ferramentas de desenvolvimento bacanas que aproveitam a loja e podem ajudar a reproduzir um determinado estado do aplicativo ou fazer viagens no tempo, por exemplo.

Simplicidade arquitetônica

Muitos dos benefícios do Ngrx podem ser alcançados com outras soluções; afinal, Redux é um padrão de arquitetura. Mas quando você precisa construir um aplicativo que se encaixe perfeitamente no padrão Redux, como ferramentas de edição colaborativa, você pode adicionar recursos facilmente seguindo o padrão.

Embora você não precise pensar no que está fazendo, adicionar algumas coisas, como análises em todos os seus aplicativos, torna-se trivial, pois você pode rastrear todas as ações que são despachadas.

Curva de aprendizado pequena

Como esse padrão é tão amplamente adotado e simples, é muito fácil para novas pessoas em sua equipe se atualizarem rapidamente sobre o que você fez.

O Ngrx brilha mais quando você tem muitos atores externos que podem modificar seu aplicativo, como um painel de monitoramento. Nesses casos, é difícil gerenciar todos os dados de entrada que são enviados para seu aplicativo e o gerenciamento de estado se torna difícil. É por isso que você deseja simplificá-lo com um estado imutável, e isso é uma coisa que a loja Ngrx nos fornece.

Construindo um aplicativo com Ngrx

O poder do Ngrx brilha mais quando você tem dados externos que estão sendo enviados para nosso aplicativo em tempo real. Com isso em mente, vamos construir uma grade de freelancers simples que mostra freelancers online e permite que você filtre por eles.

Configurando o Projeto

Angular CLI é uma ferramenta incrível que simplifica bastante o processo de configuração. Você pode querer não usá-lo, mas lembre-se de que o restante deste artigo o usará.

 npm install -g @angular/cli

Em seguida, você deseja criar um novo aplicativo e instalar todas as bibliotecas Ngrx:

 ng new toptal-freelancers npm install ngrx --save

Redutor de freelancers

Os redutores são uma peça central da arquitetura Redux, então por que não começar com eles primeiro enquanto cria o aplicativo?

Primeiro, crie um redutor “freelancers” que será responsável por criar nosso novo estado cada vez que uma ação for despachada para a loja.

freelancer-grid/freelancers.reducer.ts

 import { Action } from '@ngrx/store'; export interface AppState { freelancers : Array<IFreelancer> } export interface IFreelancer { name: string, email: string, thumbnail: string } export const ACTIONS = { FREELANCERS_LOADED: 'FREELANCERS_LOADED', } export function freelancersReducer( state: Array<IFreelancer> = [], action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); default: return state; } }

Então aqui está o nosso redutor de freelancers.

Essa função será chamada toda vez que uma ação for despachada pela loja. Se a ação for FREELANCERS_LOADED , ela criará uma nova matriz a partir da carga útil da ação. Se não for, ele retornará a referência de estado antigo e nada será anexado.

É importante observar aqui que, se a referência de estado antigo for retornada, o estado será considerado inalterado. Isso significa que se você chamar um state.push(something) , o estado não será considerado alterado. Tenha isso em mente ao fazer suas funções de redução.

Os estados são imutáveis. Um novo estado deve ser retornado sempre que for alterado.

Componente de grade do freelancer

Crie um componente de grade para mostrar nossos freelancers online. A princípio, ele refletirá apenas o que está na loja.

 ng generate component freelancer-grid

Coloque o seguinte em freelancer-grid.component.ts

 import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState, IFreelancer, ACTIONS } from './freelancer-reducer'; import * as Rx from 'RxJS'; @Component({ selector: 'app-freelancer-grid', templateUrl: './freelancer-grid.component.html', styleUrls: ['./freelancer-grid.component.scss'], }) export class FreelancerGridComponent implements OnInit { public freelancers: Rx.Observable<Array<IFreelancer>>; constructor(private store: Store<AppState>) { this.freelancers = store.select('freelancers'); } }

E o seguinte em freelancer-grid.component.html :

 <span class="count">Number of freelancers online: {{(freelancers | async).length}}</span> <div class="freelancer fade thumbail" *ngFor="let freelancer of freelancers | async"> <button type="button" class="close" aria-label="Close" (click)="delete(freelancer)"><span aria-hidden="true">&times;</span></button><br> <img class="img-circle center-block" src="{{freelancer.thumbnail}}" /><br> <div class="info"><span><strong>Name: </strong>{{freelancer.name}}</span> <span><strong>Email: </strong>{{freelancer.email}}</span></div> <a class="btn btn-default">Hire {{freelancer.name}}</a> </div>

Então o que você acabou de fazer?

Primeiro, você criou um novo componente chamado freelancer-grid .

O componente contém uma propriedade chamada freelancers que faz parte do estado do aplicativo contido no armazenamento Ngrx. Ao usar o operador select, você escolhe ser notificado apenas pela propriedade freelancers sobre o estado geral do aplicativo. Portanto, agora, cada vez que a propriedade freelancers do estado do aplicativo for alterada, seu observável será notificado.

Uma coisa bonita com esta solução é que seu componente tem apenas uma dependência, e é o armazenamento que torna seu componente muito menos complexo e facilmente reutilizável.

Na parte do modelo, você não fez nada muito complexo. Observe o uso de pipe assíncrono no *ngFor . O observable dos freelancers não é diretamente iterável, mas graças ao Angular, temos as ferramentas para desembrulhar e vincular o dom ao seu valor usando o pipe assíncrono. Isso torna o trabalho com o observável muito mais fácil.

Adicionando a funcionalidade Remover Freelancers

Agora que você tem uma base funcional, vamos adicionar algumas ações ao aplicativo.

Você deseja remover um freelancer do estado. De acordo com o funcionamento do Redux, você precisa primeiro definir essa ação em cada estado que é afetado por ela.

Neste caso, é apenas o redutor freelancers :

 export const ACTIONS = { FREELANCERS_LOADED: 'FREELANCERS_LOADED', DELETE_FREELANCER: 'DELETE_FREELANCER', } export function freelancersReducer( state: Array<IFreelancer> = [], action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); case ACTIONS.DELETE_FREELANCER: // Remove the element from the array state.splice(state.indexOf(action.payload), 1); // We need to create another reference return Array.prototype.concat(state); default: return state; } }

É realmente importante aqui criar um novo array do antigo para ter um novo estado imutável.

Agora, você pode adicionar uma função de exclusão de freelancers ao seu componente que despachará esta ação para a loja:

 delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) }

Isso não parece simples?

Agora você pode remover um freelancer específico do estado e essa alteração será propagada pelo seu aplicativo.

Agora, e se você adicionar outro componente ao aplicativo para ver como eles podem interagir entre si por meio da loja?

Filtro redutor

Como sempre, vamos começar com o redutor. Para esse componente, é bastante simples. Você deseja que o redutor sempre retorne um novo estado apenas com a propriedade que despachamos. Deve ficar assim:

 import { Action } from '@ngrx/store'; export interface IFilter { name: string, email: string, } export const ACTIONS = { UPDATE_FITLER: 'UPDATE_FITLER', CLEAR_FITLER: 'CLEAR_FITLER', } const initialState = { name: '', email: '' }; export function filterReducer( state: IFilter = initialState, action: Action): IFilter { switch (action.type) { case ACTIONS.UPDATE_FITLER: // Create a new state from payload return Object.assign({}, action.payload); case ACTIONS.CLEAR_FITLER: // Create a new state from initial state return Object.assign({}, initialState); default: return state; } }

Componente de filtro

 import { Component, OnInit } from '@angular/core'; import { IFilter, ACTIONS as FilterACTIONS } from './filter-reducer'; import { Store } from '@ngrx/store'; import { FormGroup, FormControl } from '@angular/forms'; import * as Rx from 'RxJS'; @Component({ selector: 'app-filter', template: '<form class="filter">'+ '<label>Name</label>'+ '<input type="text" [formControl]="name" name="name"/>'+ '<label>Email</label>'+ '<input type="text" [formControl]="email" name="email"/>'+ '<a (click)="clearFilter()" class="btn btn-default">Clear Filter</a>'+ '</form>', styleUrls: ['./filter.component.scss'], }) export class FilterComponent implements OnInit { public name = new FormControl(); public email = new FormControl(); constructor(private store: Store<any>) { store.select('filter').subscribe((filter: IFilter) => { this.name.setValue(filter.name); this.email.setValue(filter.email); }) Rx.Observable.merge(this.name.valueChanges, this.email.valueChanges).debounceTime(1000).subscribe(() => this.filter()); } ngOnInit() { } filter() { this.store.dispatch({ type: FilterACTIONS.UPDATE_FITLER, payload: { name: this.name.value, email: this.email.value, } }); } clearFilter() { this.store.dispatch({ type: FilterACTIONS.CLEAR_FITLER, }) } }

Primeiro, você criou um modelo simples que inclui um formulário com dois campos (nome e email) que reflete nosso estado.

Você mantém esses campos em sincronia com o estado de maneira um pouco diferente do que fez com o estado dos freelancers . Na verdade, como você viu, você se inscreveu no estado do filtro e, a cada vez, ele aciona você atribui o novo valor ao formControl .

Uma coisa legal do Angular 2 é que ele fornece muitas ferramentas para interagir com observáveis.

Você viu o pipe assíncrono anteriormente e agora vê a classe formControl que permite que você tenha um observável no valor de uma entrada. Isso permite coisas extravagantes como o que você fez no componente de filtro.

Como você pode ver, você usa Rx.observable.merge para combinar os dois observáveis ​​fornecidos por seu formControls e, em seguida, debounce esse novo observável antes de acionar a função de filter .

Em palavras mais simples, você espera um segundo após a alteração do nome ou do e-mail formControl e, em seguida, chama a função de filter .

Não é incrível?

Tudo isso é feito em poucas linhas de código. Esta é uma das razões pelas quais você vai adorar o RxJS. Ele permite que você faça muitas dessas coisas extravagantes facilmente que seriam mais complicadas de outra forma.

Agora vamos passar para essa função de filtro. O que isso faz?

Ele simplesmente despacha a ação UPDATE_FILTER com o valor do nome e do email, e o redutor se encarrega de alterar o estado com essa informação.

Vamos para algo mais interessante.

Como você faz esse filtro interagir com sua grade de freelancer criada anteriormente?

Simples. Você só precisa ouvir a parte do filtro da loja. Vamos ver como é o código.

 import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState, IFreelancer, ACTIONS } from './freelancer-reducer'; import { IFilter, ACTIONS as FilterACTIONS } from './../filter/filter-reducer'; import * as Rx from 'RxJS'; @Component({ selector: 'app-freelancer-grid', templateUrl: './freelancer-grid.component', styleUrls: ['./freelancer-grid.component.scss'], }) export class FreelancerGridComponent implements OnInit { public freelancers: Rx.Observable<Array<IFreelancer>>; public filter: Rx.Observable<IFilter>; constructor(private store: Store<AppState>) { this.freelancers = Rx.Observable.combineLatest(store.select('freelancers'), store.select('filter'), this.applyFilter); } applyFilter(freelancers: Array<IFreelancer>, filter: IFilter): Array<IFreelancer> { return freelancers .filter(x => !filter.name || x.name.toLowerCase().indexOf(filter.name.toLowerCase()) !== -1) .filter(x => !filter.email || x.email.toLowerCase().indexOf(filter.email.toLowerCase()) !== -1) } ngOnInit() { } delete(freelancer) { this.store.dispatch({ type: ACTIONS.DELETE_FREELANCER, payload: freelancer, }) } }

Não é mais complicado do que isso.

Mais uma vez, você usou o poder do RxJS para combinar o filtro e o estado dos freelancers.

Na verdade, combineLatest será acionado se um dos dois observáveis ​​for acionado e, em seguida, combinará cada estado usando a função applyFilter . Ele retorna um novo observável que faz isso. Não precisamos alterar nenhuma outra linha de código.

Observe como o componente não se importa com a forma como o filtro é obtido, modificado ou armazenado; ele apenas o ouve como faria para qualquer outro estado. Acabamos de adicionar a funcionalidade de filtro e não adicionamos novas dependências.

Fazendo Brilhar

Lembra que o uso do Ngrx realmente brilha quando temos que lidar com dados em tempo real? Vamos adicionar essa parte ao nosso aplicativo e ver como fica.

Apresentando o freelancers-service .

 ng generate service freelancer

O serviço freelancer simulará a operação em tempo real nos dados e deve ficar assim.

 import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState, IFreelancer, ACTIONS } from './freelancer-grid/freelancer-reducer'; import { Http, Response } from '@angular/http'; @Injectable() export class RealtimeFreelancersService { private USER_API_URL = 'https://randomuser.me/api/?results=' constructor(private store: Store<AppState>, private http: Http) { } private toFreelancer(value: any) { return { name: value.name.first + ' ' + value.name.last, email: value.email, thumbail: value.picture.large, } } private random(y) { return Math.floor(Math.random() * y); } public run() { this.http.get(`${this.USER_API_URL}51`).subscribe((response) => { this.store.dispatch({ type: ACTIONS.FREELANCERS_LOADED, payload: response.json().results.map(this.toFreelancer) }) }) setInterval(() => { this.store.select('freelancers').first().subscribe((freelancers: Array<IFreelancer>) => { let getDeletedIndex = () => { return this.random(freelancers.length - 1) } this.http.get(`${this.USER_API_URL}${this.random(10)}`).subscribe((response) => { this.store.dispatch({ type: ACTIONS.INCOMMING_DATA, payload: { ADD: response.json().results.map(this.toFreelancer), DELETE: new Array(this.random(6)).fill(0).map(() => getDeletedIndex()), } }); this.addFadeClassToNewElements(); }); }); }, 10000); } private addFadeClassToNewElements() { let elements = window.document.getElementsByClassName('freelancer'); for (let i = 0; i < elements.length; i++) { if (elements.item(i).className.indexOf('fade') === -1) { elements.item(i).classList.add('fade'); } } } }

Este serviço não é perfeito, mas faz o que faz e, para fins de demonstração, permite-nos demonstrar algumas coisas.

Em primeiro lugar, este serviço é bastante simples. Ele consulta uma API do usuário e envia os resultados para a loja. É um acéfalo, e você não precisa pensar para onde os dados vão. Ele vai para a loja, que é algo que torna o Redux tão útil e perigoso ao mesmo tempo—mas voltaremos a isso mais tarde. A cada dez segundos, o serviço seleciona alguns freelancers e envia uma operação para excluí-los junto com uma operação para alguns outros freelancers.

Se quisermos que nosso redutor seja capaz de lidar com isso, precisamos modificá-lo:

 import { Action } from '@ngrx/store'; export interface AppState { freelancers : Array<IFreelancer> } export interface IFreelancer { name: string, email: string, } export const ACTIONS = { LOAD_FREELANCERS: 'LOAD_FREELANCERS', INCOMMING_DATA: 'INCOMMING_DATA', DELETE_FREELANCER: 'DELETE_FREELANCER', } export function freelancersReducer( state: Array<IFreelancer> = [], action: Action): Array<IFreelancer> { switch (action.type) { case ACTIONS.INCOMMING_DATA: action.payload.DELETE.forEach((index) => { state.splice(state.indexOf(action.payload), 1); }) return Array.prototype.concat(action.payload.ADD, state); case ACTIONS.FREELANCERS_LOADED: // Return the new state with the payload as freelancers list return Array.prototype.concat(action.payload); case ACTIONS.DELETE_FREELANCER: // Remove the element from the array state.splice(state.indexOf(action.payload), 1); // We need to create another reference return Array.prototype.concat(state); default: return state; } }

Agora somos capazes de lidar com tais operações.

Uma coisa que é demonstrada nesse serviço é que, de todo o processo de mudança de estado sendo feito de forma síncrona, é muito importante notar isso. Se a aplicação do estado for assíncrona, a chamada a this.addFadeClassToNewElements(); não funcionaria, pois o elemento DOM não seria criado quando essa função fosse chamada.

Pessoalmente, acho isso bastante útil, pois melhora a previsibilidade.

Construindo aplicativos, a maneira reativa

Por meio deste tutorial, você construiu um aplicativo reativo usando Ngrx, RxJS e Angular 2.

Como você viu, essas são ferramentas poderosas. O que você construiu aqui também pode ser visto como a implementação de uma arquitetura Redux, e o Redux é poderoso por si só. No entanto, também tem algumas restrições. Enquanto usamos o Ngrx, essas restrições inevitavelmente refletem na parte do nosso aplicativo que usamos.

Paradigma Reativo

O diagrama acima é um esboço da arquitetura que você acabou de fazer.

Você pode notar que mesmo que alguns componentes estejam influenciando uns aos outros, eles são independentes uns dos outros. Esta é uma peculiaridade desta arquitetura: os componentes compartilham uma dependência comum, que é a loja.

Outra coisa particular sobre essa arquitetura é que não chamamos funções, mas despachamos ações. Uma alternativa ao Ngrx pode ser apenas fazer um serviço que gerencie um estado específico com observáveis ​​de seus aplicativos e chame funções nesse serviço em vez de ações. Dessa forma, você pode obter centralização e reatividade do estado enquanto isola o estado problemático. Essa abordagem pode ajudá-lo a reduzir a sobrecarga de criar um redutor e descrever ações como objetos simples.

Quando você sente que o estado do seu aplicativo está sendo atualizado de diferentes fontes e começa a se tornar uma bagunça, o Ngrx é o que você precisa.

Relacionado: Todas as vantagens, sem complicações: um tutorial do Angular 9