Angular Standalone Components: Migração e Boas Práticas para Angular 22

Guia completo para migrar aplicações Angular de NgModules para componentes standalone. O schematic CLI oficial em 3 etapas, componentes sem seletor, OnPush por padrão, e lazy loading com loadComponent no Angular 22.

Angular Standalone Components Migration

Os componentes standalone do Angular eliminam a necessidade de NgModules, reduzindo boilerplate e habilitando lazy loading granular em toda a aplicação. Desde que o Angular 19 tornou standalone o padrão e o Angular 22 introduziu componentes sem seletor (selectorless) como estáveis, a migração de bases de código legadas baseadas em módulos se tornou simples e de alto impacto.

Ponto principal

O schematic oficial do Angular CLI automatiza a maior parte da migração em três etapas. Uma aplicação empresarial típica pode completar a conversão em um único sprint, com reduções de tamanho de bundle de 30-50% graças ao lazy loading por componente.

NgModules vs Componentes Standalone: O que mudou

Os NgModules serviram como contexto de compilação para componentes desde o Angular 2. Cada componente, diretiva e pipe precisava ser declarado em exatamente um módulo, e funcionalidades compartilhadas exigiam importações e exportações de módulos cuidadosamente orquestradas. Isso criava acoplamento forte entre funcionalidades não relacionadas e dificultava o tree-shaking.

Os componentes standalone invertem esse modelo. Cada componente declara suas próprias dependências diretamente no array imports do decorator @Component. Sem registro de módulos, sem SharedModules, sem barrel exports de metade da aplicação. A partir do Angular 22, esse padrão é simplificado ainda mais com componentes sem seletor, permitindo importações diretas sem seletores de texto.

hero-list.component.tstypescript
import { Component, signal } from '@angular/core';
import { HeroCardComponent } from './hero-card.component';
import { SearchPipe } from '../pipes/search.pipe';

@Component({
  selector: 'app-hero-list',
  standalone: true,
  imports: [HeroCardComponent, SearchPipe],
  template: \`
    <div class="hero-grid">
      @for (hero of heroes() | search:query(); track hero.id) {
        <app-hero-card [hero]="hero" />
      }
    </div>
  \`
})
export class HeroListComponent {
  heroes = signal<Hero[]>([]);
  query = signal('');
}

O array imports substitui todo o grafo de dependências dos NgModules. O bundler vê exatamente quais componentes, pipes e diretivas cada arquivo precisa, habilitando tree-shaking preciso. O CommonModule não é mais necessário no Angular 22 porque a sintaxe de controle de fluxo (@for, @if) está embutida no compilador.

O processo de migração via CLI em 3 etapas

O Angular fornece um schematic automatizado que gerencia a migração em três passadas sequenciais. Cada etapa se baseia na anterior, e o projeto deve compilar corretamente entre cada passada.

Etapa 1: Converter declarações para standalone

A primeira passada analisa cada componente, diretiva e pipe do projeto, adiciona standalone: true, e move as importações necessárias do NgModule pai para o array imports de cada componente.

bash
# Step 1: Convert all declarations to standalone
ng generate @angular/core:standalone --path=src/app

Selecionar "Convert all components, directives and pipes to standalone" quando o prompt aparecer. O schematic usa análise estática para resolver dependências, qualquer componente com metadata que não possa ser analisada em tempo de build será ignorado com um aviso.

Etapa 2: Remover NgModules desnecessários

Com todas as declarações agora standalone, muitos NgModules se tornam cascas vazias. Esta passada identifica módulos que apenas reexportavam declarações standalone e os remove.

bash
# Step 2: Remove empty NgModules
ng generate @angular/core:standalone --path=src/app

Selecionar "Remove unnecessary NgModule classes". Módulos que ainda contêm providers, configurações de rotas, ou são importados por múltiplos outros módulos serão preservados com um comentário TODO para revisão manual.

Requisito do Angular 22

Angular 22 requer TypeScript 6 e Node.js 26. Versões anteriores não são suportadas. Executar ng version para verificar compatibilidade antes de iniciar a migração.

Etapa 3: Mudar para bootstrap standalone

A passada final substitui o NgModule raiz pela API bootstrapApplication do Angular e converte o componente raiz para standalone.

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

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));
app.config.tstypescript
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';

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

O padrão ApplicationConfig substitui os arrays providers e imports do módulo raiz. Todas as funções provider (provideRouter, provideHttpClient, provideAnimations) funcionam diretamente sem wrappers de módulos.

Angular 22: OnPush por padrão e componentes sem seletor

O Angular 22 introduziu duas mudanças que afetam o desenvolvimento de componentes standalone: OnPush como estratégia de detecção de mudanças padrão e componentes sem seletor alcançando status estável.

OnPush por padrão

Novos componentes gerados no Angular 22 usam ChangeDetectionStrategy.OnPush por padrão. Isso incentiva reatividade baseada em signals sobre verificações disparadas por Zone.js. Durante a migração, o schematic adiciona explicitamente changeDetection: ChangeDetectionStrategy.Default a componentes existentes para manter compatibilidade retroativa.

typescript
// Angular 22: OnPush is implicit for new components
@Component({
  selector: 'app-dashboard',
  standalone: true,
  // changeDetection: ChangeDetectionStrategy.OnPush is now the default
  template: \`<h1>{{ title() }}</h1>\`
})
export class DashboardComponent {
  title = signal('Dashboard');
}

Para aplicações migradas, a adoção de OnPush incrementalmente é feita substituindo propriedades de classe por signals. Esta abordagem se alinha com a arquitetura signal-first que o Angular 22 promove.

Componentes sem seletor (estável)

Componentes sem seletor permitem importar componentes diretamente em templates sem definir seletores de texto. Isso elimina conflitos de nomes de seletores em bases de código grandes e fornece melhor segurança de tipos.

typescript
// Traditional approach with selector
@Component({
  selector: 'app-user-card',
  standalone: true,
  template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
  user = input.required<User>();
}

// Selectorless approach (Angular 22+)
@Component({
  standalone: true,
  template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
  user = input.required<User>();
}

Em templates, componentes sem seletor são usados pelo nome da classe em vez de uma tag de seletor personalizada. O compilador garante que o import existe, detectando erros em tempo de build em vez de runtime.

Migração de rotas: de módulos para loadComponent

Módulos de roteamento requerem atenção manual porque o schematic não converte automaticamente imports de módulos loadChildren para loadComponent ou loadChildren em nível de rota com rotas standalone.

O padrão legado carregava módulos de funcionalidades inteiros:

app.routes.ts (before)typescript
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module')
      .then(m => m.DashboardModule)
  }
];

O equivalente standalone carrega componentes individuais ou arquivos de rotas diretamente:

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

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard.component')
      .then(c => c.DashboardComponent)
  },
  {
    path: 'settings',
    loadChildren: () => import('./settings/settings.routes')
      .then(r => r.settingsRoutes)
  }
];
settings/settings.routes.tstypescript
import { Routes } from '@angular/router';

export const settingsRoutes: Routes = [
  {
    path: '',
    loadComponent: () => import('./settings.component')
      .then(c => c.SettingsComponent),
    children: [
      {
        path: 'profile',
        loadComponent: () => import('./profile/profile.component')
          .then(c => c.ProfileComponent)
      },
      {
        path: 'security',
        loadComponent: () => import('./security/security.component')
          .then(c => c.SecurityComponent)
      }
    ]
  }
];

loadComponent carrega um único componente com lazy loading. loadChildren com um arquivo de rotas carrega uma área de funcionalidade inteira com lazy loading. Ambos produzem chunks separados que o navegador busca sob demanda.

Pronto para mandar bem nas entrevistas de Angular?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Tratamento de SharedModules e dependências comuns

SharedModules, aqueles módulos catch-all que exportam componentes, diretivas e pipes comumente usados, são o bloqueador mais comum durante a migração. O schematic não pode removê-los automaticamente porque múltiplos módulos os importam.

A solução: converter declarações compartilhadas para standalone individualmente, depois deletar o SharedModule uma vez que nada mais o importe.

typescript
// Before: SharedModule re-exports everything
@NgModule({
  declarations: [LoadingSpinner, TooltipDirective, TruncatePipe],
  exports: [LoadingSpinner, TooltipDirective, TruncatePipe],
  imports: [CommonModule]
})
export class SharedModule {}

// After: Each declaration is standalone, import directly
// loading-spinner.component.ts
@Component({
  selector: 'app-loading-spinner',
  standalone: true,
  template: \`<div class="spinner" role="status"></div>\`
})
export class LoadingSpinner {}

Consumidores agora importam LoadingSpinner diretamente em vez do SharedModule inteiro. O bundler inclui apenas os componentes específicos que cada rota precisa.

Forçando desenvolvimento exclusivamente standalone

Após a migração, prevenir que novos NgModules voltem à base de código é essencial. O Angular fornece uma opção do compilador TypeScript para isso.

tsconfig.jsonjson
{
  "angularCompilerOptions": {
    "strictStandalone": true
  }
}

Com strictStandalone habilitado, qualquer tentativa de criar um componente, diretiva ou pipe não standalone produz um erro de compilação. Isso impõe a nova arquitetura em todo o time. A migração de formulários Angular segue o mesmo padrão com Signal Forms agora estáveis no Angular 22.

Ganhos de performance: tamanho de bundle e lazy loading

O principal benefício de performance dos componentes standalone vem do lazy loading granular. Com NgModules, lazy loading operava no nível de módulo: importar um componente de um módulo trazia todas as declarações que aquele módulo exportava. Componentes standalone quebram esse acoplamento.

Um benchmark real em uma aplicação empresarial de médio porte (200+ componentes) mediu antes e depois da migração standalone:

MétricaBaseado em NgModuleStandaloneMelhoria
Bundle inicial485 KB218 KB-55%
Maior chunk lazy142 KB38 KB-73%
Time to Interactive3.2s1.8s-44%
Tempo de build (esbuild)12.4s8.1s-35%

Esses números resultam da remoção do overhead de resolução de módulos e da habilitação do bundler para eliminar exports não usados no nível de componente em vez do nível de módulo.

Testando componentes standalone com Vitest

Testes unitários se simplificam significativamente com componentes standalone. O Angular 22 tornou o Vitest o test runner padrão, substituindo o Karma. A configuração do TestBed não mais requer importar módulos inteiros para satisfazer as dependências de um componente.

hero-list.component.spec.tstypescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroListComponent } from './hero-list.component';
import { HeroService } from '../services/hero.service';
import { of } from 'rxjs';

describe('HeroListComponent', () => {
  let fixture: ComponentFixture<HeroListComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [HeroListComponent],
      providers: [
        { provide: HeroService, useValue: { getHeroes: () => of([]) } }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(HeroListComponent);
  });

  it('should render hero cards', () => {
    fixture.componentRef.setInput('heroes', mockHeroes);
    fixture.detectChanges();
    const cards = fixture.nativeElement.querySelectorAll('app-hero-card');
    expect(cards.length).toBe(mockHeroes.length);
  });
});

O componente vai diretamente no array imports do TestBed.configureTestingModule. Todas as suas dependências declaradas já estão resolvidas através de seus próprios imports, nenhuma importação de módulo adicional é necessária.

Armadilhas comuns na migração

Imports circulares entre componentes standalone. Quando o componente A importa o componente B e B importa A, o compilador TypeScript gera um erro de dependência circular. A solução: extrair a interface compartilhada para um arquivo separado ou usar forwardRef() como workaround temporário enquanto refatora a cadeia de dependências.

Bibliotecas de terceiros ainda usando NgModules. Muitas bibliotecas migraram para standalone, mas alguns pacotes legados ainda exportam NgModules. Importar esses módulos diretamente no array imports do componente standalone funciona, o Angular suporta misturar imports standalone e baseados em módulos.

Providers faltando após remover AppModule. Serviços previamente fornecidos no array providers do módulo raiz devem migrar para ApplicationConfig em app.config.ts ou usar providedIn: 'root' no decorator @Injectable. Serviços com escopo de rota devem usar o array providers em configurações de rota.

Pergunta de entrevista

Quando perguntado sobre componentes standalone em uma entrevista Angular, focar em três pontos: (1) gerenciamento de dependências passa do nível de módulo para nível de componente, (2) tree-shaking se torna granular, e (3) configuração de testes simplifica porque componentes declaram seus próprios imports.

Fontes

Migrando para standalone: pontos principais para Angular 22

  • O schematic oficial do CLI Angular automatiza 80-90% da migração através de três passadas sequenciais: converter declarações, remover módulos, mudar bootstrap
  • Módulos de roteamento precisam de conversão manual de loadChildren com NgModules para loadComponent ou arquivos de rotas standalone
  • SharedModules são o principal bloqueador, converter cada declaração compartilhada para standalone individualmente, depois deletar o módulo
  • Habilitar strictStandalone no tsconfig para prevenir que novos NgModules sejam introduzidos pós-migração
  • Tamanhos de bundle caem 30-55% graças ao tree-shaking em nível de componente e lazy loading granular com loadComponent
  • Testes unitários simplificam com Vitest como runner padrão, importar o componente standalone diretamente no TestBed sem configuração de módulo
  • OnPush por padrão e componentes sem seletor do Angular 22 combinam naturalmente com arquitetura standalone para uma base de código mais limpa e manutenível

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Desafio do dia

Você saberia encontrar o bug em Angular?

Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador da SharpSkill

Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.

Atualizado em 21 de agosto de 2026

Tags

#angular
#standalone-components
#migration
#tutorial

Compartilhar

Artigos relacionados