Compartilhando dados entre componentes Angular com Subject e BehaviorSubject

Descubra como facilitar a comunicação entre componentes no Angular usando Services em conjunto com Observable | Subject | BehaviorSubject do RxJS

Banner de divulgação: Compartilhando dados entre componentes Angular com Subject e BehaviorSubject
Compartilhando dados entre componentes Angular

Uma das características do Angular é a capacidade de dividir uma aplicação em componentes reutilizáveis, o que facilita a manutenção e o desenvolvimento. No entanto, à medida que as aplicações crescem, surge a necessidade de compartilhar dados entre componentes. Para resolver esse desafio, uma das possibilidades é através do uso de uma service.

Imagine uma service no Angular como uma espécie de "carteiro" que entrega mensagens entre diferentes componentes na sua aplicação.

Quando um componente precisa enviar informações para outro, ele coloca uma "mensagem" em uma caixa especial, que é a service. Outros componentes, que também precisam dessas informações, verificam a caixa regularmente para ver se há algo novo.

Assim, a service atua como um ponto central de comunicação, permitindo que os componentes compartilhem dados sem precisar conhecerem uns aos outros diretamente. Isso é útil quando você tem partes diferentes da sua aplicação que precisam trabalhar juntas, mas estão separadas.

Neste texto, exploraremos como utilizar uma service em conjunto com o Subject e o BehaviorSubject do RxJS, para compartilhar dados entre os componentes.

Porque utilizar uma service?

Quando decoramos uma classe com @Injectable({ providedIn: 'root' }), garantimos que existirá apenas uma instância dessa classe em todo o projeto. Isso possibilita o uso do mesmo objeto ao longo do seu projeto.

Beginning with Angular 6.0, the preferred way to create a singleton service is to set providedIn to root on the service's @Injectable() decorator. This tells Angular to provide the service in the application root.

https://angular.io/guide/singleton-services

Para ilustrar esse comportamento, criei um cenário de estudo utilizando o site StackBlitz. Ao final do texto, disponibilizo o link para o projeto completo.

O primeiro passo para criar este cenário é criar uma classe (service) com o decorator Injectable e uma variável chamada curDate que guardará o valor de uma data qualquer:

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class Example1Service {
  curDate: Date = new Date();
}

example1.service.ts

Logo em seguida criei dois componentes. A service Example1Service foi injetada em ambos, permitindo que cada componente tenha acesso ao valor da variável curDate. Além disso, adicionei um botão em cada componente com o propósito de modificar o valor de curDate:

import { Component } from '@angular/core';
import { Example1Service } from './example1.service';

@Component({
  selector: 'app-a',
  template: `
    <h1>Component A</h1>
    <div>{{ example1Service.curDate | date : 'longTime' }}</div>
    <div>
        <button (click)="update()">Atualizar</button>
    </div>`,
})
export class AComponent {
  constructor(public example1Service: Example1Service) {}

  update() {
    this.example1Service.curDate = new Date();
  }
}

a.component.ts

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Example1Service } from './example1.service';

@Component({
  selector: 'app-b',
  template: `
    <h1>Component B</h1>
    <div>{{ example1Service.curDate | date : 'longTime' }}</div>
    <div>
        <button (click)="update()">Atualizar</button>
    </div>
`,
})
export class BComponent {
  constructor(public example1Service: Example1Service) {}

  update() {
    this.example1Service.curDate = new Date();
  }
}

b.component.ts

Na sequência, criei um módulo para declarar os componentes e exportá-los, permitindo assim sua utilização no componente App (conforme demonstrado no arquivo main.ts mais abaixo):

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { AComponent } from './a.component';
import { BComponent } from './b.component';

@NgModule({
  declarations: [AComponent, BComponent],
  imports: [CommonModule],
  exports: [AComponent, BComponent],
})
export class Example1Module {}

example1.module.ts

Finalmente, incluí o módulo e os componentes no arquivo main.ts para realizar o teste:

import 'zone.js/dist/zone';
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { bootstrapApplication } from '@angular/platform-browser';
import { Example1Module } from './example1/example1.module';

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, Example1Module],
  template: `
    <app-a></app-a>
    <app-b></app-b>
  `,
})
export class App {}

bootstrapApplication(App);

main.ts

O resultado ficou da seguinte forma:

Dois componentes renderizados na tela. Quando um componente atualiza o valor o outro componente tem acesso a este novo valor
Resultado

Observe a imagem acima, onde podemos notar que, ao atualizarmos o valor da variável curDate, essa mudança é refletida em ambos os componentes. Isso acontece porque, ao clicarmos em um dos botões, o valor da variável curDate é atualizado além de desencadear o ciclo de detecção de mudanças do Angular em resposta ao clique. Durante esse ciclo, o Angular percorre a árvore de componentes para atualizar o que cada um está renderizando na tela. Como ambos os componentes compartilham a mesma fonte de informação, example1Service.curDate, ambos exibem o mesmo valor na tela.

Até este ponto já explicamos como podemos compartilhar dados entre componentes utilizando uma service. Entretanto, a forma como escrevemos a solução pode não funcionar corretamente se utilizarmos a estratégia OnPush.

Resumo sobre detecção de mudanças

Antes de prosseguir, vou deixar uma pequena explicação sobre como o Angular atualiza os dados na tela. Por padrão, sempre que ocorre algum evento, como por exemplo um clique ou um disparo de um setTimeout em qualquer parte do código, o Angular busca por mudanças em todos os componentes. Quando marcamos um componente com o OnPush, o Angular só busca por alterações naquele componente diante alguns casos:

  1. Se algum @Input do componente for modificado
  2. Se o componente emite um evento
  3. Se o componente está utilizando algum Observable em conjunto com o AsyncPipe

O objetivo do OnPush é economizar processamento e aumentar a performance.

Analogia

Pense no Angular como um detetive que verifica se algo mudou em sua casa.

  • Default (Padrão): O detetive verifica todos os cômodos o tempo todo, mesmo se nada mudou. É como se ele entrasse em cada quarto a cada minuto, mesmo que você esteja apenas assistindo TV.
  • OnPush: Agora, o detetive é mais esperto. Ele só verifica um quarto se ouvir barulho lá dentro. Ele não vai até a cozinha se ninguém está lá. Ele economiza energia e só age quando algo realmente muda.

O "ciclo de detecção de mudanças" é o processo que o detetive (Angular) usa para descobrir se algo no quarto (componente) mudou. Ele olha para os detalhes e decide se vale a pena investigar. O OnPush é como ter um detetive mais eficiente, só se preocupando quando há motivos para isso.

Como funciona o Change Detection do Angular
Veja como funciona o change detection (CD) do Angular e qual a melhor estratégia para o seu componente: Default ou OnPush.

Agora que fizemos um rápido resumo sobre o uso do OnPush, veja o resultado abaixo quando alteramos o componente A adicionando a opção changeDetection: ChangeDetectionStrategy.OnPush:

Tela do navegador mostrando os 2 componentes em execução. Ao clicar no "Atualizar" do componente A a data é atualizada na tela para os 2 componentes. Ao clicar no "Atualiza" do componente B, somente o componente B tem o dado atualizado na tela
Resultado ao alterar a estratégia de atualização de um dos componentes para OnPush

Então, quando clicamos no botão "Atualizar" do componente B, nenhuma das condições mencionadas anteriormente é satisfeita. Consequentemente, o componente A não é incluído no fluxo de detecção de mudanças do Angular.

Para resolver esse comportamento, podemos recorrer ao uso de Observables, como descrito na situação de número 3, logo acima. Para isto vamos falar sobre o Subject e o BehaviorSubject.

Subject e BehaviorSubject

O Subject, assim como o BehaviorSubject, é um tipo de Observable. A diferença entre eles é que, ao realizar um .subscribe em um Subject, começamos a receber valores a partir daquele momento específico. Por outro lado, no BehaviorSubject, ao realizar o .subscribe, você imediatamente recebe o último valor emitido. Para uma compreensão mais clara, observe o exemplo abaixo com o Subject:

const subject = new Subject<string>();
subject.next('1');
subject.asObservable().subscribe(valor => console.log(valor));
subject.next('2');

// Resultado no console:
// 2

Por outro lado, o BehaviorSubject mantém um estado interno (o último valor emitido) e fornece esse valor imediatamente aos novos observadores, além de futuros valores emitidos.

const behaviorSubject = new BehaviorSubject<string>('1');
behaviorSubject.asObservable().subscribe(valor => console.log(valor));
behaviorSubject.next('2');

// Resultado no console:
// 1
// 2

Agora que entendemos o que é o Subject e BehaviorSubject, vamos criar o segundo cenário de estudo:

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class Example2Service {
  curDate = new BehaviorSubject<Date>(new Date());

  getCurDate(): Observable<Date> {
    return this.curDate.asObservable();
  }

  updateCurDate(): void {
    this.curDate.next(new Date());
  }
}

example2.service.ts

No componente abaixo deixei a estratégia OnPush e utilizei o | async no template para observar as mudanças no curDate$.

ℹ️ Informação

É comum utilizar o sufixo $ em variáveis do tipo Observable. Isto torna mais fácil identificar quais variáveis são observáveis em seu código

import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Example2Service } from './example2.service';

@Component({
  selector: 'app-d',
  template: `
    <h1>Component D</h1>
    <div>Observable com async: {{ curDate$ | async | date : 'longTime' }}</div>
    <div>
        <button (click)="update()">Atualizar</button>
    </div>`,
})
export class DComponent {
  curDate$: Observable<Date>;

  constructor(public example2Service: Example2Service) {
    this.curDate$ = example2Service.getCurDate();
  }

  update() {
    this.example2Service.updateCurDate();
  }
}

d.component.ts

Por fim criei um novo módulo para declarar os componentes C e D:

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { CComponent } from './c.component';
import { DComponent } from './d.component';

@NgModule({
  declarations: [CComponent, DComponent],
  imports: [CommonModule],
  exports: [CComponent, DComponent],
})
export class Example2Module {}

example2.module.ts

No arquivo main.ts:

import 'zone.js/dist/zone';
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { bootstrapApplication } from '@angular/platform-browser';
import { Example1Module } from './example1/example1.module';
import { Example2Module } from './example2/example2.module';

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, Example1Module, Example2Module],
  template: `
    <h1>Example 1</h1>
    <app-a></app-a>
    <app-b></app-b>

    <h1>Example 2</h1>
    <app-c></app-c>
    <app-d></app-d>
  `,
})
export class App {}

bootstrapApplication(App);

main.ts

Resultado:

Considerações

Embora não seja a única abordagem para compartilhar dados entre componentes Angular, o uso de um serviço é, na minha opinião, uma solução bastante funcional, especialmente em cenários mais simples.

Link do projeto no StackBlitz:

https://stackblitz.com/edit/compartilhando-dados-entre-componentes-angular?file=src%2Fmain.ts