Signals, quanto mais reativo melhor!

Wesley Soares de Souza
TOTVS Developers
Published in
8 min readAug 1, 2023

--

O Angular na versão 16 veio com um novo recurso para mudar a forma para codificar nossos templates e a mudança dinâmica dos dados no formulário. Isso trouxe uma melhoria na detecção de alterações no Angular, com um ganho no desempenho e tornando a estrutura do código mais ‘reativa’. Signals foi disponibilizado na nova versão em Maio de 2023. Vamos dar uma olhada em como funciona essa nova maneira de tratar conteúdos de forma reativa neste artigo.

Porque o Signal é importante?

Vamos verificar esse exemplo bem simples:

let nome = ‘João’
let sobrenome = ' da Silva'
let completo = nome + sobrenome
console.log(completo)

Esse código simples exibe como resultado ‘João da Silva’, porém, caso o sobrenome seja corrigido:

let nome = ‘João’
let sobrenome = ' da Silva'
let completo = nome + sobrenome
console.log(completo)

sobrenome = ‘ de Nóbrega’
console.log(completo)

Ainda estaremos exibindo ‘João da Silva’ pois a variável ‘completo’ recebeu sua atribuição antes de se mudar o valor de ‘sobrenome’, porém é muito importante que nossas variáveis possam reagir a mudanças de forma dinâmica.

Através da utilização do signals, em nosso exemplo, o código ficaria assim:

let nome = signal(‘João’)
let sobrenome =signal( ' da Silva')
let completo = computed(() => nome() + sobrenome())
console.log(completo)

sobrenome.set(' de Nóbrega')
console.log(completo)

Observando atentamente o nosso código, foram definidos dois signal’s: nome e sobrenome que possuíam o valor inicial de ‘João’ e ‘ da Silva’. Foi então utilizado o método computed() na variável completo, o qual recebia a concatenação das strings. Quando um signal modifica o seu valor, através da alteração do sobrenome, por exemplo, todos os valores utilizados no método computed(), são automaticamente recalculados. Dessa forma, temos um código reativo de verdade!

Um computed signal reage e recalcula quando seus signals dependentes mudam. Quando um signal está inserido em um template, e esse signal muda, o Angular detecta que algo foi modificado automaticamente e atualiza qualquer template que esteja lendo esse signal, dessa forma o usuário percebe a alteração de imediato!

Podemos dizer que os Signal’s:

  • Provêm mais reatividade ao código
  • Usando o Signal’s temos um controle mais simples para detectar mudanças, que pode nos dar ganho de performance.

Mas o que realmente é um Signal?

Um signal é como um repositório que contém um valor. Você pode passar o repositório da mesma forma que passa o valor para o resto do seu aplicativo. Um componente pode, a qualquer momento, examinar o repositório (ler) ou atualizar o que está no repositório (gravar).

Mas por ser um repositório, ele “sabe” quando a leitura ou gravação aconteceu. Portanto, agora um “observador” pode “saber” quando uma leitura ou gravação ocorreu. Ser capaz de “saber” quando há uma leitura/gravação é o que torna os signal’s um mecanismo reativo!

Fonte: Modificado pelo autor (original)

Com base nessas informações, podemos definir com característica de um signal:

  • Uma variável com notificação de mudanças
  • É reativo, e é chamado como ‘reactive primitive
  • Sempre possui um valor
  • É síncrono

Dessa forma pode ser utilizado em:

  • Componentes para monitorar seu estado
  • Em diretivas
  • Em services para compartilhar o estado do componente

Formas de declarar um Signal

A sintaxe para criar um signal:

let test = signal<number>(1)

Ao atribuirmos um signal à uma variável, inicializamos com um valor em seu construtor, temos a opção também, de informar um parâmetro como tipo genérico com o tipo que será sendo informado ao signal. Um signal sempre deve ser iniciado com um valor padrão, o interessante é que podemos passar qualquer formato de dado a um signal, como demonstrado abaixo:

let quantidade = signal(1)
let valoresDisponiveis = signal([5,10,15,20)]
let produtoSelecionado<Produto>({
id: 1,
descricao: 'paçoca',
valor: 19.99
})
let produtos = signal<Produto[]>([]);

No primeiro código criamos um signal com valor inicial 1. Com isso a variável quantidade é iniciada com um valor numérico, dessa forma o tipo genérico não é necessário. Diferentemente no segundo código, temos um vetor de números, que são alimentados com múltiplos de 5, mais uma vez não usamos o tipo genérico, pois ao ser inferido o valor padrão ele já entende que tipo de dado deverá receber.

Na variável ‘produtoSelecionado’, recebemos um signal contendo um objeto do tipo Produto, devido ao tipo não poder ser inferido, nesse caso, especificamos um tipo genérico de Produto.

A variável ‘produtos’ é um vetor de objetos do tipo Produto, por padrão é definido como fortemente tipado através do tipo genérico definido como <Produto>

Após a criação de um signal e atribuirmos um valor em seu construtor, nós podemos atribuir um novo valor, atualizar o valor com base no dado especificado anteriormente, ou modificar seu conteúdo.

Lendo o valor de um Signal

Representamos um Signal como uma caixa, falando metaforicamente, para se ler o valor contido nele, é necessário abrir a caixa, da seguinte forma:

let teste = quantidade()

Dessa forma, ocorre a chamada da função Getter, que é criada de forma intrínseca quando se declara um Signal. No Angular podemos também realizar a leitura deste signal em um template:

<div>Produto: {{ produtoSelecionado().descricao }}</div>
<div>Preço: {{ produtoSelecionado().valor }}</div>
<div [style.color]="color()">Total: {{ valorTotal() }}</div>

No exemplo citado, foram criadas três divs, a primeira lendo o Signal produtoSelecionado, acessando a propriedade descrição, na segunda, a propriedade valor, e na última acessando o Signal valorTotal, contendo, teoricamente, o total da compra realizada.

É importante salientar que ao se ler um Signal, temos acesso ao seu valor atual, não existe possibilidade, até o momento na versão 16 do Angular, de se obter o valor anterior à alguma modificação do signal.

Alterando o valor de um Signal

O método set substitui o valor de um signal pelo novo valor informado. Basicamente, voltando a nossa analogia, seria como abrir a caixa, remover seu conteúdo e inserir um novo em seu lugar.

this.quantidade.set(10);

Um cenário comum é alterar o valor do signal com base em uma ação em tela, como por exemplo:

  • O usuário seleciona uma nova quantidade usando o elemento select
  • A ligação de evento de elemento select chama o método onQuantidadeSelecionada() e passa a quantidade selecionada, no código exibido à seguir
  • A ação do usuário é tratada nesse manipulador de eventos dentro do componente
  • O novo valor é definido no signal de quantidade.
onQuantidadeSelecionada(qty: number) {
this.quantidade.set(qty);
}

Se um template lê um signal, esse template é notificado quando o signal muda e a visualização é programada para ser renderizada novamente. Assim, o ato de ler um signal registra o interesse do consumidor em assistir a esse signal. A equipe do Angular chama isso de regra de ouro dos componentes de signal: “a detecção de alteração de um componente será agendada quando e somente quando um signal lido no modelo notificar o Angular de que foi alterado”.

Se um signal for alterado, qualquer consumidor interessado em ler esse signal é notificado. Se você estiver familiarizado com RxJS e Observables, os signal’s são bem diferentes. Os signal’s não emitem valores como os Observables. E os signal’s não exigem assinatura.

Além do método set(), temos mais dois outros caminhos para atualizar um signal, o método uodate() e mutate(). O set(), como já demonstramos, substitui com um novo valor, informado como parâmetro.

O update() atualiza o signal baseado em seu valor atual. Sendo que o parâmetro pode ser passado como uma arrow function. A arrow function provê o valor do signal atual podendo ser manipulado se necessário, como no próximo exemplo que seu valor é dobrado:

this.quantidade.update(qty => qty * 2);

O método mutate() modifica o conteúdo de um valor do signal, e não o valor em si. Deve ser usado com arrays para modificar os elementos do array e objetos para modificar as propriedades dele. No modelo a seguir o preço de um produto é aumentado em 20%:

this.produtoSelecionado.mutate(v => v.valor = v.valor + (v.valor * .20));

Independentemente de como o signal é modificado, os consumidores são notificados de que o signal foi alterado. Os consumidores podem então ler o novo valor do signal quando for sua vez de executar.

Como definir um computed Signal?

É comum termos em nosso código variáveis que dependem de outras variáveis, por exemplo o preço total de um produto depende de seu valor unitário e da quantidade de itens. Se mudarmos o valor da quantidade, esperamos que o total também mude, para isso, usamos os computed signal’s.

Definimos um computed signal criando uma arrow function criando assim um signal dependente de outro signal. A operação lê o valor de um ou mais signal’s para realizar sua computação.

valorTotal = computed(() => this.produtoSelecionado().valor * this.qunatidade());
color = computed(() => this.valorTotal() > 50000 ? 'green' : 'blue');

A primeira linha do código define que o valorTotal é um computed signal sendo chamado no metodo computed(), a função passada pelo método computed() leio o signal produtoSelecionado() e quantidade. Caso outros signal’s mudem, este computed signal será atualizado quando for executado. A segunda linha define a cor que é alterada para verde ou azul, dependendo do signal valorTotal(). O template pode se vincular a este signal para exibir o estilo apropriado. Um computed signal, não pode ser alterado com set(), update() ou mutate().

Um signal computed só será atualizado se:

  • Um ou mais signal’s dependentes mudarem
  • E o valor do computed signal estiver sendo lido.

Como usar o Effect

Pode haver momentos em que você precise executar o código quando um signal mudar e esse código tiver efeitos colaterais. Por efeitos colaterais, quero dizer código que chama uma API ou executa outra operação não relacionada ao signal. Nesses casos, você usará um effect().

Por exemplo, você deseja depurar seus signal’s e desconectar o valor do signal sempre que o código reagir a uma alteração nesse sinal. Chamar console.log() é um efeito colateral.

Para definir um effect(), chame a função de criação, conforme demonstrado a seguir, passe para essa função a operação a ser realizada, esta operação será reexecutada toda vez que o código reage a uma mudança em qualquer signal dependente.

effect(() => console.log(this.produtoSelecionado()));

A função effect() pode ser chamada dentro de outra função. Uma vez que o efeito configura uma espécie de manipulador, ele geralmente é chamado no construtor ou em outro código de inicialização.

Alternativamente, um efeito pode ser definido declarativamente como mostrado abaixo:

e = effect(() => console.log(this.produtoSelecionado()));

Um effect não deve alterar o valor de nenhum signal. Se você precisar alterar um signal com base na alteração de um signal dependente, use um computed signal.

Você descobrirá que não usará effects com frequência. Embora sejam úteis para registrar ou chamar outras APIs externas. (Mas não os use para trabalhar com RxJS e Observables. Haverá recursos de signal para converter de e para Observables.)

Quando usamos o signal?

Aqui estão algumas sugestões para quando usar signal’s. Primeiro, continue a usar manipuladores de eventos em um componente como você faz agora para ações do usuário. Ações como uma seleção em uma lista suspensa, um clique em um botão ou uma entrada em uma caixa de texto.

Use um signal ou um computed signal em um componente para qualquer estado que possa mudar. Nesse contexto, o estado refere-se a quaisquer dados que o componente gerência. Tudo, desde um sinalizador isLoading até a “página” de dados exibida atualmente, até os critérios de filtro selecionados pelo usuário, podem ser singal’s. Os signal’s são especialmente úteis ao exibir dados no modelo quando esses dados devem reagir a outras ações.

Coloque signal’s compartilhados em serviços. A matriz de produtos retornada em um Observable pode ser transformada em um signal. Quaisquer totais também podem ser signal’s em um serviço se esses signal’s forem compartilhados entre os componentes.

Continue a usar Observables para operações assíncronas, como http.get(). Existem mais recursos chegando aos signal’s para mapear um signal de e para um Observable.

Signal’s representam um grande avanço na programação reativa com Angular na sua capacidade de detectar mudanças de forma dinâmica.

Referências

Signals Angular Oficial

free code camp

--

--