Migrando BehaviorSubject para Signal em formulários Angular — passo a passo
Por John Pires · 11 de março de 2026

Contexto: quando voce vai usar isso
Quando usar
Use este guia quando você tem formulários Angular que hoje são alimentados por BehaviorSubject (ou outros Observables) e quer migrar a lógica para Signal — a API reativa baseada em sinais do Angular. Cenários típicos: formulários de edição que recebem estado inicial do servidor, formulários com pré-carregamento via rota, ou componentes standalone que consumem um serviço de estado.
Por que migrar
Signals trazem simplicidade: leitura síncrona do valor atual, menos boilerplate de subscriptions/unsubscriptions e integração com a reatividade do Angular para atualização de templates. Para iniciantes isso reduz erros comuns como fugas de memória e lógica assíncrona difícil de debugar. Mas a migração exige atenção em como sincronizar sinais com FormControls e nos efeitos colaterais.
Vou mostrar o caminho em modo code-review: primeiro o jeito errado (o que muitos fazem quando tentam migrar rápido), depois a correção com explicações. Foco em formulários reativos (ReactiveFormsModule), components standalone, injeção de dependência via inject() e integração com rotas.
Dica: pense em BehaviorSubject como uma caixa que emite atualizações e em Signal como uma variável reativa. A diferença prática: signal lê direto e dispara re-render automático para quem a consome.
Explicacao visual com diagramas mentais
Analogia simples
Analogia: BehaviorSubject é como um alto-falante que anuncia mudanças; você precisa ligar um fone (subscribe) para ouvir e lembrar de desligar. Signal é como uma variável compartilhada na memória que qualquer um pode ler — quando muda, quem está observando automaticamente é notificado, sem precisar 'ligar o fone'.
Diagrama mental
Diagrama mental mínimo para o fluxo de formulário:
- Serviço de estado: expõe dados (BehaviorSubject ou Signal).
- Componente: cria FormGroup e sincroniza com dados do serviço.
- Template: vincula FormControls para exibir/editar.
- Routing: fornece id para carregar registro e popular o serviço.
Ao migrar, substituímos o nó 'BehaviorSubject -> subscribe -> setValue' por 'signal -> effect -> setValue' ou por chamadas diretas em respostas a rota. O erro comum é tentar usar a signal como se fosse Observable sem entender a leitura síncrona.
Primeiro exemplo simples
Versão com BehaviorSubject (errado: boilerplate e leaks)
Contexto: serviço expõe BehaviorSubject para os dados iniciais do formulário; o componente se inscreve em ngOnInit e cancela em ngOnDestroy. Isso funciona, mas é verboso e sujeito a vazamentos se esquecer o unsubscribe.
O código abaixo mostra uma versão típica; depois explico o problema e migro para signal.
// profile-state.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
export interface Profile { id: string; name: string; email: string }
@Injectable({ providedIn: 'root' })
export class ProfileStateService {
private profileSubject = new BehaviorSubject<Profile | null>(null);
readonly profile$ = this.profileSubject.asObservable();
constructor(private http: HttpClient) {}
loadProfile(id: string) {
this.http.get<Profile>(`/api/profiles/${id}`).subscribe(p => this.profileSubject.next(p));
}
updateLocal(profile: Profile) {
this.profileSubject.next(profile);
}
}
O componente se inscreve:
// profile-form.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Subscription } from 'rxjs';
import { ProfileStateService } from './profile-state.service';
@Component({ selector: 'app-profile-form', template: `...` })
export class ProfileFormComponent implements OnInit, OnDestroy {
form: FormGroup;
sub = new Subscription();
constructor(private fb: FormBuilder, private state: ProfileStateService) {
this.form = this.fb.group({ id: [''], name: [''], email: [''] });
}
ngOnInit() {
this.sub.add(this.state.profile$.subscribe(p => {
if (p) this.form.patchValue(p);
}));
}
ngOnDestroy() { this.sub.unsubscribe(); }
}
O problema: muita boilerplate, necessidade de gerenciar subscriptions manualmente e lógica duplicada quando mais de um componente precisa do mesmo estado.
Migração rapida para Signal (errado: uso indevido)
Um erro comum ao migrar: usar toSignal direto no construtor e pensar que ele sempre refletirá atualizações, ou usar signal mas mutar objetos internos sem criar novo valor, o que evita notificações.
// profile-state-wrong.service.ts
import { Injectable } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ProfileStateWrongService {
private profileSubject = new BehaviorSubject<any>(null);
readonly profileSignal = toSignal(this.profileSubject.asObservable());
// problema: toSignal já cria um signal, mas se você mutar o objeto retornado, o signal pode não disparar corretamente
updateName(name: string) {
const cur = this.profileSignal();
if (cur) { cur.name = name; /* mutação direta */ }
// não chama next() no subject; sinal pode não perceber.
}
}
Depois, o componente poderia ler profileSignal() diretamente — mas a mutação direta do objeto quebra a reatividade. Esse é o tipo de erro que aprisiona iniciantes: mistura de paradigmas.
Segundo exemplo mais realista
Cenário real: formulário de perfil com sinais corretos
Abordagem correta: manter a fonte de verdade no serviço, usar signal como estado primário (ou criar signal a partir do observable, controlando atualização), expor métodos para alterar o estado e usar effect para sincronizar com o FormGroup. Evite mutações diretas.
Abaixo está um serviço que usa Signal nativo do Angular. Ele é standalone-friendly e testável.
// profile-state-signal.service.ts
import { Injectable, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export interface Profile { id: string; name: string; email: string }
@Injectable({ providedIn: 'root' })
export class ProfileStateSignalService {
private profile = signal<Profile | null>(null);
readonly profile$ = this.profile.asReadonly(); // leitura segura
constructor(private http: HttpClient) {}
loadProfile(id: string) {
// chamada HTTP que atualiza a signal
this.http.get<Profile>(`/api/profiles/${id}`).subscribe(p => this.profile.set(p));
}
updateLocal(profile: Profile) {
// Sempre setar um novo objeto (evitar mutação do objeto atual)
this.profile.set({ ...profile });
}
get profileName() {
return computed(() => this.profile()?.name ?? '');
}
}
Contexto: o serviço mantém a signal e dá métodos para carregar e atualizar. Observadores no componente não precisam unsubscribe, pois signals não são Observables; o template reagirá automaticamente quando você consumir a signal através de bindings ou quando efeitos forem estabelecidos no componente.
Componente standalone integrando FormGroup
A integração correta entre FormGroup e signals deve manter o formulário como a fonte de verdade local das alterações (FormControl), e sincronizar o FormGroup quando a signal de estado mudar. Use effect para aplicar alterações da signal ao form, e subscribe (ou valueChanges) para propagar mudanças do form de volta ao serviço.
// profile-form-standalone.component.ts
import { Component, effect, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { ProfileStateSignalService } from './profile-state-signal.service';
@Component({
selector: 'app-profile-form-standalone',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form">
<input formControlName="name" />
<input formControlName="email" />
</form>
`
})
export class ProfileFormStandaloneComponent {
private state = inject(ProfileStateSignalService);
form = inject(FormBuilder).group({ id: [''], name: [''], email: [''] });
constructor() {
// aplicar dados da signal para o form quando mudarem
effect(() => {
const p = this.state.profile$();
if (p) this.form.patchValue(p, { emitEvent: false });
});
// propagar mudanças do form para o serviço
this.form.valueChanges.subscribe(v => {
if (v && v.id) this.state.updateLocal({ ...v });
});
}
}
O que acontece: quando o serviço chama loadProfile(), a signal muda e o effect no componente executa, chamando patchValue. valueChanges é usado para enviar alterações de volta ao serviço. Não há unsubscribe do effect; o Angular gerencia sua vida útil junto com o componente.
Integração com routing e dependency injection
Carregando via rota (exemplo prático)
Frequentemente o parâmetro de rota contém o id do recurso a ser editado. Em components standalone você pode usar ActivatedRoute via inject().
// profile-route.component.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProfileStateSignalService } from './profile-state-signal.service';
@Component({ selector: 'app-profile-route', template: `<app-profile-form-standalone></app-profile-form-standalone>`, standalone: true })
export class ProfileRouteComponent {
private route = inject(ActivatedRoute);
private state = inject(ProfileStateSignalService);
constructor() {
const id = this.route.snapshot.paramMap.get('id');
if (id) this.state.loadProfile(id);
}
}
Contexto: usando snapshot é simples e síncrono para o carregamento inicial. Se precisar reagir a mudanças de rota, use route.paramMap.subscribe ou use effect combinado com a rota (evite mixar sinais de rota diretamente sem ler a documentação).
Dependência e escopo do serviço
Decida o provider scope: em muitos casos o serviço de estado deve ser providedIn: 'root' para ser singleton entre rotas; em outros, você quer escopo por rota e então forneceria o serviço no componente de rota. Isso influencia se o estado persiste quando o usuário navega para fora e volta.
Resumo prático: signals facilitam a leitura síncrona e diminuem gerenciamento manual de subscriptions; mas escolha bem o escopo do serviço.
Erros comuns de iniciante e como evitar
Leaky subscriptions (subscribe sem unsubscribe)
Erro: manter vários subscribe() em componentes e esquecer de cancelar — causa memory leaks e múltiplas atualizações inesperadas. Com signals, você reduz esse risco ao usar effect() em vez de subscribe(). Quando usar Observables (ex: HttpClient), prefira operadores como take(1) ou transformar o observable em signal com cuidado.
Mutação de sinais
Erro: alterar propriedades internas do objeto armazenado em uma signal em vez de criar um novo objeto. Exemplo: fazer const p = profileSignal(); p.name = 'x' não dispara notificação se não houver clonagem e depende de referências. Solução: sempre usar set() com novo objeto: profile.set({ ...profile(), name: 'x' }).
Outros problemas relacionados:
- Converter Observable para Signal com
toSignal()sem entender o valor inicial e a forma como a cancelamento funciona. - Usar
effectpara fazer chamadas HTTP sem debouncing pode disparar muitas requisições. - Tentar usar signal diretamente em template com forms template-driven — prefira ReactiveForms quando for sincronizar sinais.
Dica: para sincronizar formulário e signal, trate o FormGroup como fonte de verdade enquanto o usuário edita e atualize a signal somente quando fizer save ou em valueChanges com debounce.
Conclusao pratica
Checklist de migração
Checklist rápido para migrar um formulário de BehaviorSubject para Signal sem surpresas:
- Decida o escopo do serviço (root vs componente/route).
- Substitua BehaviorSubject por
signal()no serviço e exponha leitura comasReadonly()se necessário. - Evite mutação: sempre chame
set()com novo objeto. - No componente, use
effect()para sincronizar signal -> form eform.valueChangespara propagar form -> signal (use debounce quando necessário). - Teste rotas e lifecycle para garantir que efeitos não disparam em loops.
Proximos passos
Depois da migração básica, explore: computed para valores derivados, toObservable quando precisa interoperar com APIs RxJS, e padrões de state management com sinais (pequenas stores). Sempre escreva testes unitários para assegurar que os efeitos sincronizam corretamente e que não há regressões no comportamento do formulário.
Este guia mostrou o caminho do jeito errado ao certo, com exemplos práticos e funcionais. A técnica principal é: mantenha no serviço a fonte de verdade com signals, evite mutações e use effects para sincronizar com formulários. Assim você reduz boilerplate e ganha previsibilidade.
Implementar sinais em formulários é um exercício de disciplina: sinais para estado, FormControls para edição local, efeitos para sincronização. Se seguir esse padrão, seu código ficará mais simples, menos propenso a leaks e mais fácil de testar.
Para referência, verifique a documentação do Angular sobre Signals, ReactiveFormsModule, e a interop entre RxJS e sinais (toSignal, toObservable).
Assuntos: Angular, signals, rxjs, forms, standalone-components, dependency-injection, routing
Leia também
Prefetch de dados por rota no Angular com Standalone Components e Signals
Aprenda a prefetch de dados por rota no Angular usando Standalone Components e Signals: combine ResolveFn com um PrefetchStore reativo para reduzir latência…
Prefetch inteligente de rotas com Signals e RxJS em Standalone Components
Prefetch inteligente para Angular: combine Signals para estado e RxJS para orquestração de intents (hover, focus, intersection). Exemplo prático com…
Providers com escopo de rota em Angular standalone: signals, ciclo de vida e limpeza
Guia prático para usar providers com escopo de rota em Angular standalone: exemplos com Signals, factories que leem params de rota, limpeza via onCleanup e…
Wizard multi-step com rotas filhas, Signals e Standalone Components
Tutorial prático e direto para criar um wizard multi-step em Angular: rotas filhas para cada etapa, gerenciamento de estado com Signals e componentes…
Autenticação reativa com Guards, Signals e Standalone Components em Angular (passo a passo)
Tutorial prático para iniciantes: implemente autenticação reativa em Angular usando signals, standalone components e guards. Passo a passo com exemplos…
Cancelar requisições ao navegar: AbortController, Signals e RxJS em componentes standalone Angular
Padrões práticos para cancelar requisições HTTP ao navegar em Angular: combinar AbortController, rxjs/fromFetch e signals em components standalone para evitar…