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.
- 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>
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');
}
}
- o segundo é por update que define com base no valor atual;
app.component.html
<p>
Nosso Signal: {{exemploCount()}}
</p>
<button (click)="executar()">Executar</button>
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);
}
}
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>
contador.component.css
:host {
display: block;
}
.container {
display: flex;
gap: 10px;
}
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);
}
}
Qual a jogada nesse código, tem agora o signal que adicionamos um valor inicial na linha:
contador = signal(0);
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);
}
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>
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);
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>
contador-v2.component.css
:host {
display: block;
}
.container {
display: flex;
gap: 10px;
}
.result{
margin-top: 10px;
}
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);
}
}
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);
}
}
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>
contador-v3.component.css
:host {
display: block;
}
.container {
display: flex;
gap: 10px;
}
.result{
margin-top: 10px;
}
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);
}
Na app.component, temos o effect.
<app-contador-v3></app-contador-v3>
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')
}
}
});
}
}
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);
}
}
Console sendo incrementado:
Tela não sendo alterada:
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);
}
}
A tela o valor agora é incrementado:
app.component.html
<app-on-push-teste></app-on-push-teste>
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() {
}
}
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:
Top comments (0)