Destino Dev

Blog

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.

  1. Esquecer de importar RouterModule no componente standalone: sem isso, diretivas como routerLink e router-outlet não funcionarão. Solução: adicionar RouterModule em imports.

  2. Navegar sem caminho relativo: usar this.router.navigate(['/wizard/step-2']) funciona, mas perde portabilidade. Preferir caminhos relativos com relativeTo ou navegação por rotas filhas se estiver no contexto correto.

  3. Modificar directly o sinal (mutabilidade): não se deve modificar objetos internos sem usar update ou set. Usar this._data.update(prev => ({...prev, novo: val})).

  4. 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