Voltar ao Blog
Angular Development30 de dezembro de 20258 min de leitura

Angular Standalone Components: O Novo Padrão

DR

Doug Ribeiro

8 min de leitura

NgModule confundiu quase todo mundo que encontrou Angular pela primeira vez. Eu vi desenvolvedores React seniores passarem dois dias entendendo por que um componente que eles declararam no FeatureModule não estava disponível em outro FeatureModule. A resposta envolvia SharedModule, forFeature(), e um grafo de dependências que era completamente invisível no nível do componente. Standalone components corrige isso eliminando a indireção.

O Que Standalone Realmente Significa

Um standalone component declara suas próprias dependências diretamente no seu array de imports. Sem wrapper de NgModule. Sem array de declarations em outro lugar. Se um componente precisa de RouterLink, ele importa RouterLink. Se precisa de CurrencyPipe, importa CurrencyPipe. O arquivo do componente é auto-descritivo.

typescript
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { AccountCardComponent } from './account-card.component';
import { Account } from './account.model';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [
    RouterLink,
    AccountCardComponent
  ],
  template: `
    <h1>Dashboard</h1>
    <a routerLink="/accounts">All accounts</a>
    @for (account of accounts; track account.id) {
      <app-account-card [account]="account" />
    }
  `
})
export class DashboardComponent {
  accounts: Account[] = [];
}

Perceba que AccountCardComponent é importado diretamente. Ele não precisa estar em um módulo compartilhado. O import por si só te diz: este componente usa AccountCardComponent. Sem precisar rastrear declarations de módulos. Repare que o array de imports carrega só o que o template realmente usa. Qualquer outra coisa é peso morto, e o diagnóstico NG8113 do Angular vai te avisar disso.

Abordagem com NgModuleComponente declarado no Feature…FeatureModule importa SharedMod…SharedModule importa CommonModu…CommonModule fornece NgIf, NgFor4 arquivos para rastrear uma de…Barrel exports obrigatórios par…Abordagem standaloneComponente importa NgIf diretoComponente importa RouterLink d…Sem declaração de móduloA dependência fica visível no c…1 arquivo para rastrear uma dep…Tree-shaking funciona por compo…VS

Bootstrapping Sem AppModule

Projetos novos gerados com ng new não criam mais AppModule. Eles usam bootstrapApplication com um ApplicationConfig.

typescript
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig).catch(console.error);

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './core/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor]))
  ]
};

Lazy Loading por Componente

Antes de standalone, a menor unidade que podia ser lazy-loaded era um módulo. Se você queria fazer lazy-load de um único componente de página, precisava envolvê-lo em um módulo primeiro. Agora você usa loadComponent diretamente.

typescript
// app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () =>
      import('./features/dashboard/dashboard.component')
        .then(m => m.DashboardComponent)
  },
  {
    path: 'accounts',
    loadChildren: () =>
      import('./features/accounts/accounts.routes')
        .then(m => m.ACCOUNTS_ROUTES)
  }
];

Para áreas de feature com múltiplas rotas, eu ainda agrupo em um arquivo de rotas e uso loadChildren. Isso mantém a fronteira de chunk no nível da feature. Para páginas individuais que são genuinamente standalone, loadComponent é a escolha certa.

Providers com Escopo Sem Módulos

Providers de NgModule eram como você dava escopo a services em uma feature carregada lazily. Sem NgModule, você dá escopo a providers via o array providers na definição da rota.

typescript
{
  path: 'lending',
  loadComponent: () =>
    import('./features/lending/lending.component').then(m => m.LendingComponent),
  providers: [
    LendingService,
    { provide: LENDING_API_URL, useValue: '/api/v2/lending' }
  ]
}

Neste exemplo, o LendingService tem escopo na rota da feature de lending. Ele é criado na primeira vez que a rota é ativada, e nada fora da feature de lending consegue injetá-lo. Uma pegadinha: por padrão o Angular não destrói o injector de uma rota quando você navega para fora, então o service e o estado dele seguem vivos por toda a vida da aplicação. O Angular 21.1 adicionou withExperimentalAutoCleanupInjectors, que destrói o injector de uma rota assim que essa rota sai da árvore de rotas ativas, mas é experimental e você precisa ligar na mão. Enquanto não ligar, resete o estado da feature explicitamente na entrada da rota em vez de supor uma instância nova.

Migrando um Projeto Existente

O time do Angular disponibiliza schematics que automatizam a maior parte da migração. Execute-os em sequência.

  • ng generate @angular/core:standalone --mode=convert-to-standalone: converte declarations para standalone
  • ng generate @angular/core:standalone --mode=prune-ng-modules: remove módulos vazios após a conversão
  • ng generate @angular/core:standalone --mode=standalone-bootstrap: atualiza main.ts para usar bootstrapApplication
  • Revise o diff cuidadosamente para setup de providers, especialmente se você usa padrões forRoot()
  • Atualize o TestBed: standalone components usam imports ao invés de declarations no configureTestingModule

Na Wonderlabs, levei a plataforma de cartões de crédito do Angular 13 para o Angular 15 em um monorepo. As APIs standalone ficaram estáveis na 15.0 e o schematic de migração para standalone chegou na 15.2, então é a partir dessa linha de versões que uma conversão como essa fica viável. O schematic lida com a maior parte do trabalho. O que sobra é configuração de providers no AppModule que precisa de tradução manual para funções provideX(). Reserve meio dia por projeto.

O Impacto Prático

Depois da migração, a mudança mais imediata é a velocidade de onboarding. Uma pessoa desenvolvedora nova consegue entender do que um componente depende lendo o arquivo do componente. Eles não precisam mais saber em qual módulo ele vive, qual SharedModule exporta CommonModule, e por que RouterModule precisa ser importado em três lugares. Essa sobrecarga cognitiva era real, e agora sumiu.

O tamanho do build também melhora em projetos onde as fronteiras de módulo eram grosseiras. Tree-shaking é mais preciso quando o grafo de imports vive no nível do componente. Em uma área de feature que se apoia em um SharedModule excessivamente amplo, o bundle inicial pode ficar visivelmente menor após a migração apenas por eliminar os imports que esse módulo estava puxando.

Standalone components não adicionaram novas capacidades. Eles removeram indireção. Esse é um problema de engenharia mais difícil de resolver, e importa mais.

Referências

AngularTypeScriptFrontendBest Practices