DEV Community

Wanderson Alves Rodrigues
Wanderson Alves Rodrigues

Posted on

Signal Angular: Gerenciamento de estado reativo

O Angular está constantemente evoluindo, e uma das mais recentes inovações introduzidas ao ecossistema é o Signal. Este recurso, inspirado por conceitos de reatividade, promete mudar a forma como desenvolvedores Angular lidam com estados reativos e comunicação entre componentes.

Exploraremos o que é o Signal, como ele funciona, e como pode ser usado para tornar suas aplicações Angular mais performáticas e fáceis de manter.

O que é o Signal no Angular?

São uma nova API reativa introduzida no Angular que simplifica o gerenciamento e a reatividade de dados em aplicações,baseados em um padrão chamado Observer Design Pattern. De acordo com esse padrão, temos um Publisher que armazena algum valor junto com uma lista de Subscribers que estão interessados ​​nele, e quando o valor muda, eles recebem uma notificação.

Image description
Em essência, o Signal:

  • Armazena um valor reativo.
  • Notifica os consumidores automaticamente quando o valor muda.
  • Facilita a escrita de código reativo.

Por que usar Signals no Angular?

  • Simplicidade: Simplificam a criação de estados reativos. Comparados aos Observables do RxJS, eles exigem menos código para configurar e são mais intuitivos para iniciantes no Angular.

  • Desempenho: Otimizam atualizações de estado e visualizações. Apenas os componentes diretamente dependentes de um Signal são atualizados, evitando renderizações desnecessárias.

  • Menos Dependências: Não dependem do RxJS para cenários básicos. Isso reduz a curva de aprendizado e facilita a transição para o Angular.

  • Escalabilidade: São projetados para funcionar perfeitamente em arquiteturas complexas.

Como Funcionam no Angular?

Os Signals no Angular seguem três conceitos principais:

  • Criação de Signals: Você cria um Signal para armazenar um valor reativo.

  • Leitura de Signals: Você pode acessar o valor do Signal diretamente.

  • Atualização de Signals: Você pode alterar o valor e notificar os consumidores.

Vamos colocar a mão na massa!

Para alterar o valor do signal.

  • O primeiro é por set que define o signal para um novo valor;

app.component.html

<p>
Nosso Signal: {{exemploSignal()}}
</p>

<button (click)="executar()">Executar</button>
Enter fullscreen mode Exit fullscreen mode

app.component.ts

import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    RouterOutlet,
  ],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
})
export class AppComponent {
  title = 'exemplos';

  protected exemploSignal = signal('angular');

  constructor() {
  }

  executar(){
    this.exemploSignal.set('Framework Angular');
  }
}
Enter fullscreen mode Exit fullscreen mode
  • o segundo é por update que define com base no valor atual;

app.component.html

<p>
Nosso Signal: {{exemploCount()}}
</p>

<button (click)="executar()">Executar</button>
Enter fullscreen mode Exit fullscreen mode

app.component.ts

import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    RouterOutlet,
  ],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
})
export class AppComponent {
  title = 'exemplos';

  protected exemploCount = signal(1);

  constructor() {
  }

  executar(){
    this.exemploCount.update(atual => atual + 1);
  }
}
Enter fullscreen mode Exit fullscreen mode

1 - Exemplo Contador

Para o primeiro exemplo, vamos desenvolver um contador bem simples para testa o conhecimento.

contador.component.html

<div class="container">
  <button (click)="incrementar()">Incrementar</button>
  <label>Contador: {{ contador() }}</label>
  <button (click)="decrementar()">Decrementar</button>
  <button (click)="limpar()">Limpar</button>
</div>
Enter fullscreen mode Exit fullscreen mode

contador.component.css

:host {
  display: block;
}

.container {
  display: flex;
  gap: 10px;
}
Enter fullscreen mode Exit fullscreen mode

contador.component.ts

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-contador',
  standalone: true,
  imports: [],
  templateUrl: './contador.component.html',
  styleUrl: './contador.component.css'
})
export class ContadorComponent {
contador = signal(0);

  incrementar(){
    this.contador.update(valor => valor + 1);
  }

  decrementar(){
    this.contador.update(valor => valor - 1);
  }

  limpar(){
    this.contador.set(0);
  }
}
Enter fullscreen mode Exit fullscreen mode

Qual a jogada nesse código, tem agora o signal que adicionamos um valor inicial na linha:

contador = signal(0);
Enter fullscreen mode Exit fullscreen mode

Nas funções incrementar e decrementar, usamos a função update do writable signal para pegar o valor anterior e incrementar ou decrementar um valor novo.

  incrementar(){
    this.contador.update(valor => valor + 1);
  }

  decrementar(){
    this.contador.update(valor => valor - 1);
  }
Enter fullscreen mode Exit fullscreen mode

Para limpar foi usado,a função set para adicionar um novo valor porém não considerando o valor anterior.

Agora como podemos usar a atualização do valor no html?. É bem simples para mostrar o valor alterado na visualização e só chamar como fosse uma "função".

  <label>Contador: {{ contador() }}</label> 
Enter fullscreen mode Exit fullscreen mode

2 - Computed signals

Signals computados são signals somente leitura que derivam seu valor de outros signals. Você define signals computados usando a função computed e especificando uma derivação:

  contador = signal(0);
  contadorVezesDois: Signal<number> = computed(() => this.contador() * 2);
Enter fullscreen mode Exit fullscreen mode

No nosso exemplo, quando o valor do contador for atualizado o contadorVezesDois será alterado pois depende do contador.

o Código completo:

contador-v2.component.html

<div class="container">
  <button (click)="incrementar()">Incrementar</button>
  <label>Contador: {{ contador() }}</label>
  <button (click)="decrementar()">Decrementar</button>
  <button (click)="limpar()">Limpar</button>
</div>

<div class="result">
  <label>Valor do contador vezes 2: {{ contadorVezesDois() }}</label>
</div>
Enter fullscreen mode Exit fullscreen mode

contador-v2.component.css

:host {
  display: block;
}

.container {
  display: flex;
  gap: 10px;
}

.result{
  margin-top: 10px;
}
Enter fullscreen mode Exit fullscreen mode

contador-v2.component.ts

import { Component, computed, Signal, signal } from '@angular/core';

@Component({
  selector: 'app-contador-v2',
  standalone: true,
  imports: [],
  templateUrl: './contador-v2.component.html',
  styleUrl: './contador-v2.component.css'
})
export class ContadorV2Component {
  contador = signal(0);
  contadorVezesDois: Signal<number> = computed(() => this.contador() * 2);

  incrementar(){
    this.contador.update(valor => valor + 1);
  }

  decrementar(){
    this.contador.update(valor => valor - 1);
  }

  limpar(){
    this.contador.set(0);
  }
}
Enter fullscreen mode Exit fullscreen mode

3 - Effects

Signal notificam os consumidores interessados ​​quando eles mudam. Effect é uma operação que é executada sempre que um ou mais valores de Signal mudam.

Para esse exemplo, vamos criar um serviço root.

import { Injectable, signal } from "@angular/core";

@Injectable({ providedIn: 'root' })
export class Store {
  contador = signal(0);

  incrementar(){
    this.contador.update(valor => valor + 1);
  }

  decrementar(){
    this.contador.update(valor => valor - 1);
  }

  limpar(){
    this.contador.set(0);
  }
}
Enter fullscreen mode Exit fullscreen mode

O componente contador agora vai ficar assim.

contador-v3.component.html

<div class="container">
  <button (click)="store.incrementar()">Incrementar</button>
  <label>Contador: {{ store.contador() }}</label>
  <button (click)="store.decrementar()">Decrementar</button>
  <button (click)="store.limpar()">Limpar</button>
</div>
Enter fullscreen mode Exit fullscreen mode

contador-v3.component.css

:host {
  display: block;
}

.container {
  display: flex;
  gap: 10px;
}

.result{
  margin-top: 10px;
}
Enter fullscreen mode Exit fullscreen mode

contador-v3.component.ts

import { Component, inject } from '@angular/core';
import { Store } from '../store';

@Component({
  selector: 'app-contador-v3',
  standalone: true,
  imports: [],
  templateUrl: './contador-v3.component.html',
  styleUrl: './contador-v3.component.css'
})
export class ContadorV3Component {
 protected store = inject(Store);
}
Enter fullscreen mode Exit fullscreen mode

Na app.component, temos o effect.

<app-contador-v3></app-contador-v3>
Enter fullscreen mode Exit fullscreen mode
import { Component, effect, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { ContadorV3Component } from './contador-v3/contador-v3.component';
import { Store } from './store';
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    RouterOutlet,
    ContadorV3Component,
  ],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
})
export class AppComponent {
  protected store = inject(Store)

  constructor() {
    effect(() => {
      const contador = this.store.contador();
      if(contador < 0) {
        console.log('Negativo')
      }
      else{
      const par = contador % 2 === 0;
      if(par) {
        console.log('Par')
      } else {
        console.log('Impar')
      }
    }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Então quando incremento, decremento ou limpar será notificado ao effect, onde fiz uma simples impressão em console.

  • effect será acionado pelo menos uma vez.
  • effect será acionado após pelo menos um dos Signal dos quais ele depende (lê seu valor) mudar.
  • effect será chamado um número mínimo de vezes. Isso significa que se vários Signals dos quais o effect depende mudarem seus valores ao mesmo tempo, o código será executado apenas uma vez.

4 - On Push Compooent

O Angular só verificará as alterações quando o input for modificado ou algum evento for disparado. Portanto, se o seu componente estiver usando changeDetection: ChangeDetectionStrategy.OnPush, as alterações só serão refletidas na DOM nos casos citados.

No primeiro exemplo, vamos criar um atributo chamado valor e colocar um temporizador no construtor que vai incrementar. Mesmo com a incrementação do valor não afeta a exibição no DOM.

on-push-teste.component.ts

import { ChangeDetectionStrategy, Component, signal } from "@angular/core";

@Component({
  selector: 'app-on-push-teste',
  standalone: true,
  template: `
    <h1>OnPush Teste</h1>
    <p>O valor é: {{valor}}</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushTesteComponent {
valor = 1;

constructor(){
  setInterval(() => {
    this.valor++;
    console.log('Mudou o valor: ', this.valor);
  }, 1000);
}
}
Enter fullscreen mode Exit fullscreen mode

Console sendo incrementado:

Image description

Tela não sendo alterada:

Image description

Porém quando utilizamos signal o comportamento fica totalmente diferente, como segue abaixo:

on-push-teste.component.ts

import { ChangeDetectionStrategy, Component, signal } from "@angular/core";

@Component({
  selector: 'app-on-push-teste',
  standalone: true,
  template: `
    <h1>OnPush Teste</h1>
    <p>O valor é: {{valor()}}</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushTesteComponent {
valor = signal(1);

constructor(){
  setInterval(() => {
    this.valor.update(valor => valor + 1);
    console.log('Mudou o valor: ', this.valor());
  }, 1000);
}
}
Enter fullscreen mode Exit fullscreen mode

A tela o valor agora é incrementado:

Image description

app.component.html

<app-on-push-teste></app-on-push-teste>
Enter fullscreen mode Exit fullscreen mode

app.component.ts

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { OnPushTesteComponent } from './on-push-teste/on-push-teste.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    RouterOutlet,
    OnPushTesteComponent
  ],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
})
export class AppComponent {

  constructor() {
  }
}
Enter fullscreen mode Exit fullscreen mode

Por qual motivo isso acontece?. Como agora definimos a valor = signal(1); como signal, qualquer alteração nele notifica que precisa se renderizado o novo valor.

O código completo: Github

Referência:

Angular Signals

Top comments (0)