Desenvolvimento web e mobile
Fundamentos de Angular: componentes standalone, signals e as APIs modernas
Intermediário9 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
O Angular mudou bastante entre a era dos NgModules e as versões atuais: os componentes são standalone por padrão, o estado fica em signals, os templates usam blocos de control flow nativos e a detecção de mudanças pode rodar sem Zone.js. Este guia ensina o framework como ele é hoje, desde um primeiro componente até injeção de dependência, HTTP, rotas, formulários reativos e o modelo de detecção de mudanças que amarra tudo isso.
O que você vai aprender
- Criar componentes standalone e entender como uma aplicação inicializa sem NgModules
- Fazer binding de dados nos templates com os blocos de control flow nativos e os signal inputs
- Modelar estado com signals, valores computados e effects, e saber quando o RxJS ainda é a ferramenta certa
- Injetar serviços e configurar o HttpClient e o router com funções de provider
- Construir e validar formulários reativos, e explicar como a detecção de mudanças decide o que renderizar de novo
Se a última vez que você usou Angular foi na época de NgModule, dos decorators @Input() e do *ngFor, o framework que você vai encontrar hoje está diferente em quase todos os arquivos. Os componentes são standalone, o estado fica em signals, os templates usam sintaxe de blocos para condicionais e loops, os providers vêm de funções em vez de metadados de módulo e a detecção de mudanças pode dispensar totalmente o Zone.js. A arquitetura (componentes, templates, injeção de dependência, um router) continua reconhecível; a ergonomia, não.
Este guia ensina a forma atual do framework, e não a histórica. Ele começa com um único componente, adiciona estado, serviços, HTTP e rotas, e depois explica o modelo de detecção de mudanças que torna todo o resto previsível. Tudo aqui pressupõe TypeScript, que o Angular não apenas tolera, mas do qual depende: metadados de decorators, tokens de injeção e formulários tipados se apoiam nele.
A unidade de uma aplicação: o componente
Um componente é uma classe TypeScript com um decorator que a associa a um template e a alguns estilos. Nada mais é necessário: nenhum módulo o declara, nenhum arquivo barrel o registra.
import { Component, signal, computed, input, output } from '@angular/core';
@Component({
selector: 'app-order-row',
template: `
<tr [class.selected]="selected()">
<td>{{ order().reference }}</td>
<td>{{ order().total | currency: 'EUR' }}</td>
<td><button type="button" (click)="remove.emit(order().id)">Remove</button></td>
</tr>
`,
styles: `.selected { background: var(--highlight); }`,
})
export class OrderRowComponent {
readonly order = input.required<Order>();
readonly selected = input(false);
readonly remove = output<string>();
}
Vale destacar algumas coisas. input() e output() são funções, não decorators, e input.required<T>() faz o compilador exigir que quem usa o componente forneça o valor. Um input criado assim é um signal: você o lê chamando-o, e tudo o que deriva dele se atualiza automaticamente. O seletor é a tag usada onde o componente aparece; o array imports (omitido aqui porque o template não usa outros componentes) é onde um componente standalone lista os componentes, diretivas e pipes de que seu template precisa.
Aqui o TypeScript pesa de verdade. Interfaces descrevem as estruturas que cruzam as fronteiras entre componentes, generics tipam os inputs e as respostas HTTP, e ativar strict no tsconfig.json é a diferença entre o compilador pegar um null e o navegador reportá-lo.
Uma aplicação começa fazendo o bootstrap de um componente raiz com uma lista de providers:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withFetch(), withInterceptors([authInterceptor])),
],
});
Templates: bindings e control flow com blocos
Templates Angular são HTML com quatro formas de binding. A interpolação com chaves duplas renderiza um valor como texto. Colchetes fazem binding de uma propriedade: [disabled]="isBusy()" define a propriedade do DOM, não o atributo. Parênteses fazem binding de um evento: (click)="save()". E a “banana na caixa” [(ngModel)] combina as duas direções, sendo apenas açúcar sintático para um property binding mais um event binding.
A lógica estrutural agora usa blocos nativos no lugar das antigas diretivas com asterisco:
@Component({
selector: 'app-orders',
template: `
@if (orders().length > 0) {
<table>
@for (order of orders(); track order.id) {
<app-order-row [order]="order" (remove)="delete($event)" />
} @empty {
<tr><td>Nothing to show</td></tr>
}
</table>
} @else {
<p>Loading…</p>
}
@switch (status()) {
@case ('draft') { <app-draft-banner /> }
@case ('submitted') { <app-submitted-banner /> }
@default { <app-unknown-banner /> }
}
@defer (on viewport) {
<app-revenue-chart [data]="chartData()" />
} @placeholder {
<div class="skeleton"></div>
}
`,
imports: [OrderRowComponent, DraftBannerComponent],
})
export class OrdersComponent { /* … */ }
Os blocos fazem parte da linguagem de templates, e não são diretivas: não é preciso importar nada, o compilador consegue checar tipos dentro deles e o código gerado é menor e mais rápido. A expressão track no @for é obrigatória, e faz diferença: ela diz ao Angular como casar os itens entre uma renderização e outra, para que ele possa mover nós do DOM em vez de destruí-los e recriá-los. Rastrear por índice em uma lista que muda de ordem é uma fonte clássica de perda do estado dos campos do formulário. O @defer é um recurso realmente novo: ele separa em outro chunk tudo o que está dentro dele e carrega esse conteúdo a partir de um gatilho, como entrar no viewport, passar o mouse por cima ou um callback de ociosidade.
Pipes transformam um valor no template. currency, date, json, async e keyvalue cobrem a maioria das necessidades, e um pipe puro customizado é uma forma barata de formatar sem adicionar uma chamada de método a cada passada da detecção de mudanças.
Signals: estado que o framework consegue rastrear
Um signal é um contêiner de valor que registra quem o lê. Quando ele muda, exatamente esses leitores são notificados, e nada mais é reavaliado.
import { Component, signal, computed, effect } from '@angular/core';
@Component({ /* … */ })
export class CartComponent {
readonly items = signal<CartItem[]>([]);
readonly coupon = signal<string | null>(null);
readonly subtotal = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0),
);
readonly total = computed(() =>
this.coupon() ? this.subtotal() * 0.9 : this.subtotal(),
);
constructor() {
effect(() => localStorage.setItem('cart', JSON.stringify(this.items())));
}
add(item: CartItem) {
this.items.update((current) => [...current, item]); // new array, not push
}
}
Você lê um signal chamando-o. Escreve com set para substituir o valor ou com update para calculá-lo a partir do anterior. computed deriva um valor de forma lazy e o mantém em cache até que uma de suas dependências mude, então o total acima só é recalculado quando items ou coupon mudam. effect executa efeitos colaterais quando suas dependências mudam e serve para sincronizar com coisas de fora do Angular (storage, uma biblioteca de gráficos, logging), não para escrever de volta em outros signals.
A regra que faz tudo isso funcionar é a imutabilidade. this.items.update(list => [...list, item]) produz um array novo e notifica os leitores; this.items().push(item) altera o array no lugar e não notifica ninguém. É a mesma disciplina descrita em fundamentos de JavaScript, só que aqui o framework depende dela.
Signals também estão por trás das APIs de query e de model: viewChild() e contentChild() retornam signals do elemento encontrado, e model() cria um signal input com binding bidirecional.
Injeção de dependência e serviços
O injetor do Angular é hierárquico: ele começa no componente e sobe pelos componentes pais até a raiz da aplicação. Um serviço é uma classe comum marcada como injetável, e providedIn: 'root' o transforma em um singleton que é removido pelo tree-shaking se ninguém o injetar.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class OrderService {
private readonly http = inject(HttpClient);
list(page: number) {
return this.http.get<Page<Order>>('/api/orders', { params: { page } });
}
create(draft: OrderDraft) {
return this.http.post<Order>('/api/orders', draft);
}
}
A função inject() substituiu em grande parte a injeção por parâmetros do construtor. Ela funciona em inicializadores de campos, em funções factory e dentro dos helpers provide*, e é componível: você pode escrever uma pequena função reutilizável que também chama inject(). A injeção pelo construtor continua funcionando e ainda é o que você vai ver em arquivos mais antigos.
Tokens de injeção permitem injetar coisas que não são classes (um objeto de configuração, uma URL base, uma feature flag), e fornecer outra implementação para o mesmo token é o jeito de trocar um serviço real por um fake nos testes ou por um backend simulado em desenvolvimento.
HTTP, e para onde vão os erros
O HttpClient retorna observables frios: nada é enviado até que alguém se inscreva, e cancelar a inscrição cancela a requisição. Configure-o uma única vez com provideHttpClient(withFetch()), que usa por baixo a implementação de fetch da plataforma.
this.orders.list(1).subscribe({
next: (page) => this.items.set(page.content),
error: (error: HttpErrorResponse) => this.message.set(describe(error.status)),
});
Na API atual, interceptors são funções comuns e são o lugar certo para headers de autenticação, IDs de correlação, política de retry e mapeamento centralizado de erros:
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(TokenStore).current();
return next(token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req);
};
Diferentemente do fetch, o HttpClient trata uma resposta fora da faixa 2xx como erro, então o tratamento de status fica no callback de erro. Decidir o que cada código significa para o usuário (tentar de novo, autenticar novamente, mostrar uma mensagem de validação) merece ser feito de forma deliberada; a referência de códigos de status HTTP cobre as diferenças, e design de APIs REST cobre o lado servidor do mesmo contrato.
As versões mais recentes adicionam uma API de resource que encapsula um carregamento assíncrono em signals, expondo valor, estado de carregamento e erro como signals e refazendo a busca quando o signal da requisição muda. Ela elimina boa parte do boilerplate de “carregue isto quando aquilo mudar”, e vale a pena conferir na documentação atual antes de implementar isso na mão.
Rotas
As rotas são um array comum, e o lazy loading é um import dinâmico:
export const routes: Routes = [
{ path: '', component: HomeComponent, title: 'Home' },
{
path: 'orders',
loadChildren: () => import('./orders/routes').then((m) => m.ORDER_ROUTES),
canActivate: [authGuard],
},
{ path: 'orders/:id', loadComponent: () => import('./orders/detail').then((m) => m.OrderDetailComponent) },
{ path: '**', component: NotFoundComponent },
];
loadComponent e loadChildren dividem o bundle na fronteira da rota, que é a decisão de performance de maior impacto em uma aplicação grande. Guards são funções que retornam um booleano, uma UrlTree ou um observable de um dos dois. Com withComponentInputBinding() ativado, parâmetros de rota, query params e dados resolvidos são ligados diretamente aos inputs do componente, então um componente que lê id = input.required<string>() não precisa injetar o router para nada.
Formulários reativos
Formulários reativos definem o modelo em TypeScript, o que o torna tipado e testável sem renderizar nada.
private readonly fb = inject(NonNullableFormBuilder);
readonly form = this.fb.group({
reference: this.fb.control('', [Validators.required, Validators.maxLength(20)]),
email: this.fb.control('', [Validators.required, Validators.email]),
quantity: this.fb.control(1, [Validators.min(1), Validators.max(50)]),
});
submit() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
this.orders.create(this.form.getRawValue()).subscribe(/* … */);
}
Cada controle expõe value, valid, errors, dirty, touched e um observable valueChanges. Validadores customizados são funções que retornam um mapa de erros ou null, e validadores assíncronos retornam um observable: é o jeito comum de checar um valor no servidor enquanto o usuário digita, com debounce. Só mostre um erro depois que o controle estiver touched ou o formulário tiver sido enviado; validar assim que o usuário digita o primeiro caractere é o erro de usabilidade mais comum em formulários Angular.
Como a detecção de mudanças decide o que renderizar de novo
Historicamente, o Angular dependia do Zone.js, que faz monkey-patching de timers, event listeners e XHR para que o framework saiba quando qualquer coisa assíncrona termina. A cada um desses eventos, ele percorre a árvore de componentes a partir da raiz e reavalia todos os bindings de todos os templates. É por isso que o framework parecia “automático”, e também por isso que telas grandes ficavam lentas.
ChangeDetectionStrategy.OnPush restringe esse percurso: um componente só é verificado quando um input muda de referência, um evento se origina dentro dele, um signal que ele lê muda ou algo o marca como sujo. Combinado com atualizações imutáveis, isso transforma o percurso da árvore inteira em um caminho da raiz até os componentes que realmente mudaram.
Os signals vão além, porque ler um deles em um template registra uma dependência precisa. Isso torna o percurso da árvore desnecessário, e é o que viabiliza a detecção de mudanças zoneless: com provideZonelessChangeDetection(), a aplicação abandona completamente a dependência do Zone.js e agenda uma verificação quando um signal muda ou um evento dispara. Bundle menor, stack traces mais limpos e fim dos ciclos extras misteriosos. Isso exige que todo o estado lido pelo template seja um signal ou algo que marque explicitamente o componente como sujo, e é por isso que código orientado a signals é o padrão recomendado para aplicações novas.
RxJS, organização do projeto e testes
O RxJS não sumiu; seu escopo encolheu para aquilo em que ele é realmente bom: fluxos ao longo do tempo. Fazer debounce de uma caixa de busca com debounceTime, cancelar uma requisição que ficou obsoleta com switchMap, repetir com backoff, combinar várias fontes: nada disso tem equivalente em signals. A ponte idiomática é o toSignal(), que transforma um observable em um signal que o template pode ler sem o pipe async e sem cancelar inscrições manualmente. Para estado de componente que é simplesmente “o valor atual de alguma coisa”, signals exigem menos maquinário e deixam menos espaço para erro.
A CLI gera e mantém o projeto. ng new, ng generate component orders/order-row, ng serve, ng build, ng test e ng update cobrem o trabalho do dia a dia, e o ng update executa schematics que reescrevem seu código diante de breaking changes; é por isso que ficar perto da versão atual sai muito mais barato do que pular várias. Organize por funcionalidade e não por tipo: uma pasta orders com seus componentes, serviço, rotas e modelos é melhor do que árvores paralelas de components e services, que obrigam toda mudança a mexer em quatro diretórios.
Nos testes, o TestBed configura um injetor em miniatura para que um componente possa ser renderizado com providers falsos, enquanto um serviço com dependências injetadas geralmente pode ser testado como uma classe comum, sem nenhum envolvimento do framework. Teste a lógica baseada em signals definindo inputs e lendo os valores computados; reserve a cobertura end-to-end para os fluxos que atravessam vários componentes.
Principais pontos
- Componentes standalone são o padrão: sem NgModules,
bootstrapApplicationna raiz e providers fornecidos por funçõesprovide*. - Templates usam control flow com blocos (
@if,@forcomtrackobrigatório,@switche@defer), que o compilador entende diretamente. - Signals guardam o estado,
computedo deriva de forma lazy eeffectsincroniza com o mundo externo; as atualizações precisam ser imutáveis para que tudo isso funcione. inject()é o jeito moderno de chegar aos serviços, interceptors são funções comuns e os observables doHttpClientsão frios e canceláveis.- OnPush e signals reduzem a detecção de mudanças de um percurso completo da árvore a uma atualização direcionada, e é isso que torna possíveis as aplicações zoneless.
- Mantenha o RxJS para fluxos baseados em tempo e faça a ponte com
toSignal(); organize por funcionalidade e deixe ong updatelevar você de uma versão para a outra.
Fontes e leituras complementares
Perguntas frequentes
- Ainda preciso de NgModules?
- Não para código novo. Componentes, diretivas e pipes são standalone por padrão, as aplicações inicializam com bootstrapApplication e os providers vêm de funções como provideRouter e provideHttpClient. Os NgModules continuam suportados para que aplicações grandes já existentes sigam funcionando e possam migrar aos poucos, e algumas bibliotecas de terceiros ainda os distribuem, mas nada em um projeto novo exige que você declare um.
- Signals substituem o RxJS?
- Eles se sobrepõem, mas resolvem problemas diferentes. Signals modelam estado síncrono lido pelo template, com rastreamento automático de dependências e sem gerenciar inscrições. O RxJS modela fluxos de eventos ao longo do tempo e oferece operadores para fazer debounce, repetir, cancelar e combinar trabalho assíncrono. O código moderno típico usa signals para o estado dos componentes e RxJS para os pipelines de HTTP e de eventos, fazendo a ponte com toSignal.
- O que a detecção de mudanças OnPush faz de verdade?
- Por padrão, o Angular verifica todos os componentes da árvore sempre que qualquer coisa assíncrona acontece. Com OnPush, um componente só é verificado quando um de seus inputs muda de referência, um evento dispara dentro dele, um signal que ele lê muda ou ele é marcado explicitamente como sujo. Em uma tela grande, isso elimina a maior parte do trabalho de cada ciclo. Componentes baseados em signals tornam o comportamento previsível, e é isso que viabiliza aplicações zoneless.
- Formulários reativos ou formulários orientados a template?
- Prefira formulários reativos para qualquer coisa além de uma caixa de busca com dois campos. O modelo do formulário é definido em TypeScript, então ele é tipado, pode ser testado unitariamente sem renderizar e é fácil de compor, e controles dinâmicos ou validadores entre campos são simples de fazer. Formulários orientados a template colocam o modelo no markup com ngModel, o que é mais rápido em casos triviais, mas fica difícil de testar e de entender conforme a validação cresce.