Wizard multi-step com rotas filhas, Signals e Standalone Components
Por John Pires · 16 de março de 2026

Wizard multi-step com rotas filhas, Signals e Standalone Components
O que é e por que importa
Definição prática do problema
Um wizard multi-step é uma interface em que o usuário avança por etapas (steps) para completar uma tarefa — por exemplo, cadastro, checkout ou configuração. Em aplicações Angular modernas, implementá-lo de forma modular e previsível levanta decisões: manter estado local, sincronizar com URL, permitir voltar/avançar pelo navegador e compartilhar componentes.
Por que rotas filhas, Signals e standalone importam
Rotas filhas permitem mapear cada etapa para uma URL própria. Isso melhora acessibilidade, permite deep-linking e facilita navegação por históricos. Signals (API reativa do Angular) trazem uma forma simples e performática de gerenciar estado sem RxJS pesado. Standalone components simplificam a composição: cada step é um componente independente com suas próprias dependências.
Alternativas rápidas e suas limitações
É comum ver wizards implementados com: 1) um único componente controlando etapas com um índice; 2) comportamento baseado em serviços com BehaviorSubject; 3) formulários agrupados e navegação interna. Cada abordagem tem trade-offs: manter tudo em um componente torna testes e reutilização difíceis, BehaviorSubject adiciona boilerplate RxJS, e ausência de rotas impede linkabilidade.
Conceitos fundamentais com analogias
O que são rotas filhas — analogia da biblioteca
Imagine uma biblioteca com salas (rotas) e estantes (sub-rotas). As rotas filhas são como estantes dentro de uma sala: cada etapa do wizard é uma estante — acessível diretamente, mas organizada sob o contexto da sala principal (o wizard).
Signals — analogia do post-it
Signals são como um post-it colado em um monitor: qualquer componente pode ler o post-it, e quando alguém atualiza o post-it todos que olham para ele reagem imediatamente. É uma unidade de estado reativa e minimalista em comparação com streams RxJS.
Standalone components — analogia da caixa modular
Standalone components são blocos que já vêm com tudo que precisam dentro da caixa. Não precisam ser declarados em NgModules: você importa o que precisa diretamente no componente. Isso facilita testes e reuso, e reduz o acoplamento com módulos grandes.
Passo a passo com código comentado
Visão geral da solução
Vamos construir um wizard com três passos, cada passo como standalone component. Haverá um componente pai (WizardShell) que define as rotas filhas e exibe um router-outlet. O estado do wizard (dados do formulário) ficará em um WizardStore usando Signals. Os passos injetam a store e atualizam dados. A navegação entre passos usa Router com rutas relativas.
Arquivo: wizard-store.ts (o que faz)
Este serviço mantém o estado do wizard com signals: dados, passo atual e funções para avançar/voltar. O serviço é singleton (provideIn: 'root') e expõe sinais e helpers.
import { Injectable, signal } from '@angular/core';
export type WizardData = {
name?: string;
email?: string;
agree?: boolean;
};
@Injectable({ providedIn: 'root' })
export class WizardStore {
// Signal que guarda os dados do wizard
private _data = signal({});
// Expor um getter para leitura reativa
readonly data = this._data;
// Atualiza parcialmente os dados
setData(partial: Partial) {
this._data.update(prev => ({ ...prev, ...partial }));
}
// Reseta o wizard
reset() {
this._data.set({});
}
}
O código acima cria um serviço simples. Depois de injetado, qualquer componente pode ler store.data() (leitura reativa) ou chamar setData.
Arquivo: app-routes.ts (o que faz)
Configura as rotas: a rota principal '/wizard' é pai e tem rotas filhas '/wizard/step-1', '/wizard/step-2', '/wizard/step-3'. Usamos loadComponent para demonstrar lazy loading de standalone components (opcional).
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'wizard',
// Componente pai standalone
loadComponent: () => import('./wizard-shell.component').then(m => m.WizardShellComponent),
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'step-1'
},
{
path: 'step-1',
loadComponent: () => import('./steps/step1.component').then(m => m.Step1Component)
},
{
path: 'step-2',
loadComponent: () => import('./steps/step2.component').then(m => m.Step2Component)
},
{
path: 'step-3',
loadComponent: () => import('./steps/step3.component').then(m => m.Step3Component)
}
]
}
];
Depois de registrar routes no RouterModule.forRoot(routes), a navegação por URL passa a refletir o step atual.
Como um step atualiza a store (exemplo)
Mostrarei um Step básico que injeta o WizardStore, exibe campos simples e chama setData antes de navegar.
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { WizardStore } from '../wizard-store';
@Component({
selector: 'app-step1',
standalone: true,
imports: [CommonModule],
template:
Step 1 - Identificação
Nome:
Email:
Próximo
})
export class Step1Component {
name = '';
email = '';
constructor(private store: WizardStore, private router: Router) {}
next() {
this.store.setData({ name: this.name, email: this.email });
this.router.navigate(['../step-2']);
}
}
Ao clicar em 'Próximo', o componente salva dados e navega para a rota filha 'step-2'. Observe uso simples de Router; rotas filhas preservam contexto do wizard.
Exemplo prático completo
Estrutura de arquivos sugerida
Organizar o projeto ajuda iniciantes a localizar responsabilidades. Sugestão:
src/app/wizard-shell.component.ts
src/app/wizard-store.ts
src/app/steps/step1.component.ts
src/app/steps/step2.component.ts
src/app/steps/step3.component.ts
src/app/app-routes.ts
WizardShell: o container com router-outlet
O shell contém a navegação visual (percurso) e o router-outlet para steps. Ele lê a store para mostrar resumo.
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { WizardStore } from './wizard-store';
@Component({
selector: 'app-wizard-shell',
standalone: true,
imports: [CommonModule, RouterModule],
template:
1
2
3
Resumo {{ store.data() | json }}
})
export class WizardShellComponent {
constructor(public store: WizardStore) {}
}
O shell exibe um resumo reativo chamando store.data(). Quando qualquer step atualizar a store, o resumo é atualizado automaticamente.
Erros comuns e armadilhas
Ao trabalhar com rotas filhas e standalone components, iniciantes frequentemente cometem erros previsíveis. Enumerarei os principais com soluções.
Esquecer de importar RouterModule no componente standalone: sem isso, diretivas como
routerLinkerouter-outletnão funcionarão. Solução: adicionarRouterModuleemimports.Navegar sem caminho relativo: usar
this.router.navigate(['/wizard/step-2'])funciona, mas perde portabilidade. Preferir caminhos relativos comrelativeToou navegação por rotas filhas se estiver no contexto correto.Modificar directly o sinal (mutabilidade): não se deve modificar objetos internos sem usar
updateouset. Usarthis._data.update(prev => ({...prev, novo: val})).Assumir que signals substituem totalmente RxJS: signals são ótimos para estado local e direto, mas em casos de streams complexos (websockets, debounces complexos) RxJS ainda é útil.
Dica: para debug rápido de signals no template, use {{ store.data() | json }}. Ajuda a ver quando as atualizações acontecem.
Proximos passos para aprofundar
Adicionar validação e formularios reativos
Um passo natural é integrar Angular Forms. Você pode usar FormGroup dentro de cada step e sincronizar com a WizardStore apenas quando o step for validado. Alternativamente sincronize campo a campo para salvar rascunhos automáticos.
Persistência e navegação avançada
Para permitir retomada após reload, serialize a store no localStorage ou IndexedDB. Use a rota para reconstituir o estado: ex., /wizard/step-2?draftId=abc. Também considere guards para bloquear acesso a passos sem completar os anteriores.
Exemplo de guard simples (código)
Guard verifica que dados obrigatórios do step anterior existam antes de permitir acesso ao step seguinte.
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { WizardStore } from './wizard-store';
@Injectable({ providedIn: 'root' })
export class StepGuard implements CanActivate {
constructor(private store: WizardStore, private router: Router) {}
canActivate(): boolean {
const data = this.store.data();
if (!data.name) {
this.router.navigate(['/wizard/step-1']);
return false;
}
return true;
}
}
Registrar esse guard nas rotas filhas garante um fluxo sequencial rígido quando necessário.
Conclusão prática
Recapitulando decisões e benefícios
Usar rotas filhas torna o wizard navegável por URL e melhor estruturado. Signals reduzem boilerplate e tornam a reatividade direta. Standalone components promovem isolamento e reuso. Juntos, esses recursos entregam um wizard modular, testável e amigável ao roteamento.
Checklist rápido para implementar
Definir rotas filhas para cada step
Implementar WizardStore com signals
Fazer cada step standalone e injetar a store
Usar Router para avançar/voltar e fornecer links diretos
Adicionar guards/validação e persistência conforme necessário
Seguindo esse modelo você terá um wizard que funciona bem no navegador, é fácil de manter e escala com seu app.
Este artigo mostrou a solução mínima viável para iniciantes: os exemplos são intencionados para aprendizado. Ao ganhar confiança, experimente combinações com RxJS, Effects ou gerenciamento centralizado mais complexo.
Se quiser, posso gerar um repositório de exemplo pronto para clonar com todos os arquivos mencionados e instruções passo a passo para rodar localmente.
Assuntos: Angular, signals, standalone-components, routing, rxjs
Leia também
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…
Migrando BehaviorSubject para Signal em formulários Angular — passo a passo
Guia prático para migrar BehaviorSubject para Signal em formulários Angular: exemplos errados e corretos, patterns com standalone components, DI e routing, e…
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…