Angular 17 na Prática: Construindo um Dashboard Reativo com Signals, D3.js e @defer

Rocketseat

Acelere sua carreira com a Rocketseat
Aproveite a black month para garantir seu plano na melhor condição!
E aí, dev! Se você já tentou construir um dashboard interativo, provavelmente conhece a saga: a briga entre o framework e a biblioteca de gráficos, a performance que mais parece uma carroça e a complexidade que te faz questionar suas escolhas de carreira. Mas e se eu te dissesse que as coisas mudaram? E mudaram para muito melhor!
Com o lançamento do Angular 17, estamos vivendo uma verdadeira revolução. Não é apenas uma atualização com alguns remendos, mas sim, uma reimaginação da experiência de desenvolvimento. Hoje, vamos construir juntos um dashboard simples, elegante e absurdamente reativo. Vamos focar em três pilares que tornam o "novo Angular" tão especial: a sintaxe de controle de fluxo com
@if e @for, a reatividade cirúrgica com Signals e a otimização de performance com o carregamento deferido (@defer).Prepare o café e bora codar!
A revolução do Angular 17 e o nosso setup
Antes de mergulharmos no código, vamos preparar nosso ambiente. A boa notícia é que o Angular 17 tornou esse processo mais rápido e inteligente do que nunca.
Preparando o terreno com velocidade
Abra seu terminal e execute o seguinte comando:
ng new dashboard-interativo --standalone --style=scss
Enquanto o Angular CLI faz sua mágica, vamos entender o que acabamos de pedir. Com o Angular 17, o CLI já vem configurado com padrões modernos que melhoram drasticamente a nossa vida.
- Vite e esbuild por padrão: se você piscou, talvez tenha perdido o build inicial. O Angular agora usa Vite para o servidor de desenvolvimento e esbuild para o build de produção. O resultado? Uma melhoria de mais de 67% na velocidade de build. Seu projeto agora fica pronto antes mesmo de você conseguir pegar um café. Essa mudança não apenas acelera o ciclo de desenvolvimento, mas também diminui a barreira de entrada, tornando o ecossistema mais amigável e competitivo.
- Standalone components por padrão: diga adeus à necessidade de declarar cada componente em um
NgModule. Com componentes standalone como padrão, a estrutura do projeto fica mais limpa e intuitiva. Cada peça da sua aplicação é mais independente e reutilizável, removendo uma camada de abstração que historicamente era uma fonte de confusão para muitos desenvolvedores. O framework está, de forma consciente, melhorando sua própria ergonomia, focando não só na performance da aplicação, mas no bem-estar de quem a desenvolve.
D3.js: mago da matemática, não o chefe do DOM!
Com o projeto criado, é hora de trazer nosso especialista em visualização de dados: o D3.js. E aqui, precisamos estabelecer uma regra de ouro, um mantra que vai guiar toda a nossa integração.
A regra de ouro da integração
O D3.js é uma biblioteca poderosa e imperativa, nascida para selecionar elementos do DOM e manipulá-los diretamente. O Angular, por outro lado, é um framework declarativo, que gerencia o DOM para nós. Tentar misturar essas duas filosofias de forma ingênua é a receita para um código confuso, cheio de bugs e que quebra completamente o ciclo de vida e a detecção de mudanças do Angular.
Portanto, nossa regra de ouro é:
O D3.js calcula, o Angular renderiza.
Imagine D3.js como o arquiteto que desenha a planta baixa, fazendo todos os cálculos complexos de escalas, posições e formas. O Angular é a equipe de construção que pega essa planta e ergue as paredes, pinta as salas e instala as janelas, ou seja, renderiza os elementos SVG no template. Usaremos o D3 apenas para seus módulos de cálculo (
d3-scale, d3-shape, etc.) e deixaremos 100% da renderização nas mãos do Angular.Essa separação de responsabilidades é um princípio de design que leva a uma arquitetura mais robusta, modular e, crucialmente, testável. A lógica do D3 pode ser encapsulada em funções puras ou serviços, que são fáceis de testar isoladamente, enquanto os componentes Angular se concentram apenas em apresentar os dados que recebem.
Configuração e nosso primeiro componente
Vamos instalar o D3 e seus tipos:
npm install d3 npm install @types/d3 --save-dev
Agora, vamos criar um componente dedicado para o nosso gráfico de barras:
ng generate component bar-chart
Construindo o gráfico com o novo controle de fluxo
Com a filosofia estabelecida, é hora de colocar a mão na massa e construir a visualização estática do nosso gráfico. Aqui é onde a nova sintaxe de controle de fluxo do Angular 17 começa a brilhar.
Definindo dados e escalas
No arquivo
bar-chart.component.ts, vamos adicionar alguns dados mock e configurar as escalas do D3 dentro do ngOnInit.import { Component, OnInit } from '@angular/core'; import * as d3 from 'd3'; @Component({ selector: 'app-bar-chart', standalone: true, imports: [], templateUrl: './bar-chart.component.html', styleUrl: './bar-chart.component.scss' }) export class BarChartComponent implements OnInit { private data = [ { month: 'Jan', sales: 120 }, { month: 'Fev', sales: 150 }, { month: 'Mar', sales: 200 }, { month: 'Abr', sales: 80 }, { month: 'Mai', sales: 250 }, ]; public processedData: any = []; public width = 500; public height = 300; ngOnInit(): void { // 1. Configurar escalas const xScale = d3.scaleBand() .domain(this.data.map(d => d.month)) .range([0, this.width]) .padding(0.1); const yScale = d3.scaleLinear() .domain([0, d3.max(filteredData, d => d.sales) || 0]) .range([this.height, 0]); // 2. Processar os dados para o template this.processedData = this.data.map(d => ({ month: d.month, x: xScale(d.month), y: yScale(d.sales), width: xScale.bandwidth(), height: this.height - yScale(d.sales) })); } }
Aqui, usamos
d3.scaleBand para mapear nossos meses para posições no eixo X e d3.scaleLinear para mapear os valores de vendas para alturas no eixo Y. O resultado é um array processedData com todos os atributos necessários para desenhar nossos retângulos SVG.A bruxaria do @for e a importância do track
Agora, vamos ao
bar-chart.component.html. Com a nova sintaxe, nosso template fica incrivelmente limpo e legível.<svg [attr.width]="width" [attr.height]="height"> <g> @for (bar of processedData; track bar.month) { <rect [attr.x]="bar.x" [attr.y]="bar.y" [attr.width]="bar.width" [attr.height]="bar.height" fill="steelblue" /> } </g> </svg>
Note o
track bar.month. No Angular 17, o track é obrigatório. E isso é ótimo! O framework está nos forçando a adotar uma das práticas mais importantes para a performance de listas. Com o track, o Angular sabe exatamente qual item do array corresponde a qual elemento no DOM. Quando os dados mudam, em vez de destruir e recriar toda a lista, ele pode fazer atualizações, adições e remoções de forma muito mais eficiente, resultando em uma performance até 90% mais rápida em alguns cenários.Lidando com listas vazias elegantemente
E se nosso array de dados estiver vazio? Antes, precisaríamos de um
*ngIf separado. Agora, a solução está integrada à própria estrutura do loop com o bloco @empty.<svg [attr.width]="width" [attr.height]="height"> <g> @for (bar of processedData; track bar.month) { } @empty { <text x="50%" y="50%" text-anchor="middle" fill="gray"> Sem dados para exibir. Que tal um café? </text> } </g> </svg>
A inclusão de blocos como
@empty e a obrigatoriedade do track mostram uma mudança na filosofia do Angular. O framework está se tornando mais "opinativo" de uma forma positiva, internalizando soluções para problemas comuns e nos guiando para a escrita de um código que é, por padrão, mais performático e legível.Dando vida ao dashboard com a reatividade dos signals
Nosso gráfico está bonito, mas estático. É hora de adicionar interatividade usando a feature mais celebrada do Angular moderno: os Signals.
O coração da interatividade: signal()
Um
signal é um invólucro (cápsula) reativo para um valor. Quando o valor dentro dele muda, ele notifica automaticamente todos os "interessados". Vamos refatorar nosso componente para usar Signals.No
bar-chart.component.ts, vamos transformar nossos dados em um signal.import { Component, computed, signal } from '@angular/core'; //... export class BarChartComponent { // Estado reativo com Signals data = signal([ { year: 2024, month: 'Jan', sales: 120 }, { year: 2024, month: 'Fev', sales: 150 }, //... mais dados de 2024 { year: 2025, month: 'Jan', sales: 180 }, { year: 2025, month: 'Fev', sales: 220 }, //... mais dados de 2025 ]); selectedYear = signal(2024); public width = 500; public height = 300; //... }
Derivação de estado: computed()
Se
signal é o coração, computed é o cérebro da nossa reatividade. Um computed signal deriva seu valor de outros signals. Ele é preguiçoso (só calcula quando lido) e memorizado (só recalcula se uma de suas dependências mudar), o que o torna extremamente eficiente.Vamos criar um
computed signal que encapsula toda a nossa lógica de filtragem e cálculo do D3.// Dentro da classe BarChartComponent processedData = computed(() => { const year = this.selectedYear(); const filteredData = this.data().filter(d => d.year === year); if (filteredData.length === 0) { return; } const xScale = d3.scaleBand() .domain(filteredData.map(d => d.month)) .range([0, this.width]) .padding(0.1); const yScale = d3.scaleLinear() .domain() .range([this.height, 0]); return filteredData.map(d => ({ month: d.month, x: xScale(d.month), y: yScale(d.sales), width: xScale.bandwidth(), height: this.height - yScale(d.sales) })); });
Veja que incrível: o
processedData agora se atualiza "magicamente" sempre que data ou selectedYear mudarem. Nosso ngOnInit se foi, e a lógica de transformação de dados agora vive dentro de um invólucro reativo.Fechando o ciclo reativo
No template, a única mudança é chamar
processedData como uma função: processedData().@for (bar of processedData(); track bar.month) { } @empty { }
Agora, vamos adicionar botões para alterar o ano selecionado no
app.component.html (ou onde você estiver usando o app-bar-chart).<div> <button (click)="chart.changeYear(2024)">2024</button> <button (click)="chart.changeYear(2025)">2025</button> </div> <app-bar-chart #chart />
E no
bar-chart.component.ts, o método changeYear simplesmente atualiza o signal:public changeYear(year: number) { this.selectedYear.set(year); }
Pronto! Ao clicar nos botões, o
selectedYear muda, o computed recalcula os dados do gráfico e o Angular atualiza o DOM de forma eficiente. Criamos um pipeline de dados reativo, unidirecional e performático, que é a materialização perfeita da nossa "regra de ouro".O gran finale: otimização com carregamento deferido (@defer)
Nosso componente está interativo e performático. Mas em um dashboard real, teríamos vários gráficos, tabelas e widgets pesados. Carregar tudo de uma vez prejudica a experiência inicial do usuário. É aqui que o
@defer entra como o nosso ás na manga.O bloco
@defer é a solução nativa e incrivelmente simples do Angular para carregar componentes de forma preguiçosa (lazy loading).@defer em ação
Imagine que temos uma tabela de detalhes pesada. Em vez de carregá-la imediatamente, podemos deferir seu carregamento até que ela entre na tela do usuário.
HTML
<app-bar-chart /> @defer (on viewport) { <app-details-table /> }
Com
(on viewport), o app-details-table e todo o seu código só serão baixados e renderizados quando o usuário rolar a página até ele. Existem outros gatilhos poderosos, como on interaction, on hover ou when condition.Para uma experiência do usuário ainda mais refinada, podemos usar os blocos auxiliares
@placeholder, @loading e @error.@defer (on viewport) { <app-heavy-chart /> } @placeholder { <div class="chart-placeholder">Carregando gráfico...</div> } @loading(after 500ms; minimum 1s) { <app-spinner /> } @error { <p>Oops! Falha ao carregar o gráfico. Tente novamente.</p> }
A evolução da legibilidade
Para realmente apreciar o salto que o Angular 17 deu, veja esta tabela de comparação. Ela mostra como a nova sintaxe não é apenas um "açúcar sintático", mas uma melhoria fundamental na clareza e na redução de código boilerplate.
Funcionalidade | Sintaxe antiga (diretivas estruturais) | Nova sintaxe (controle de fluxo nativo) |
Condicional | <div *ngIf="condicao; else elseBlock">...</div><ng-template #elseBlock>...</ng-template> | @if (condicao) {... } @else {... } |
Loop | <li *ngFor="let item of items; trackBy: trackFn">...</li> | @for (item of items; track item.id) {... } |
Loop vazio | <div *ngIf="items.length === 0">Nada aqui!</div> | @for (...) {... } @empty { Nada aqui! } |
Switch | <div [ngSwitch]='valor'><p *ngSwitchCase=\"'A'\">A</p><p *ngSwitchDefault>Outro</p></div> | @switch (valor) { @case ('A') { A } @default { Outro } } |
Lazy loading | Roteamento complexo ou ViewContainerRef | @defer { <componente-pesado /> } |
Seu próximo nível…
Beleza, você acabou de construir um componente de dashboard que não é apenas moderno, mas também performático. Mas e quando esse grático precisar consumir dados de uma API real? Como você gerencia o estado global que alimenta múltiplos componentes? Como você lida com autenticação, formulário robustos e roteamento em nídel de aplicação?
Bom, o que você aprendeu agora é a base. O próximo nível é transformar essa habilidade em aplicações completas, prontas para o mercado.
Se você quer parar de apenas "seguir tutoriais" e começar a construir projetos complexos do zero, dominando a arquitetura de uma aplicação Angular de ponta, a Formação em Angular da Rocketseat é o seu próximo passo.
Clique aqui e conheçar como acelerar sua jornada como dev Angular.
Acelere sua carreira com a Rocketseat
Aproveite a black month para garantir seu plano na melhor condição!
Artigos_
Explore conteúdos relacionados
Descubra mais artigos que complementam seu aprendizado e expandem seu conhecimento.




