Especialização em Front-End com Angular • Demonstrações Práticas Compiláveis


💻 Código de Exemplo 18 — SSR e Hidratação Não-Destrutiva com provideClientHydration

Este exemplo apresenta a configuração corporativa de Server-Side Rendering (SSR) no Angular 18+, utilizando provideClientHydration(withEventReplay()) para hidratação não-destrutiva sem piscar a tela e proteção de código de navegador com isPlatformBrowser.

1. Configuração de Hidratação: src/app/app.config.ts

// src/app/app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideClientHydration, withEventReplay, withHttpTransferCacheOptions } from '@angular/platform-browser';
import { routes } from './app.routes';
import { provideHttpClient, withFetch } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient(withFetch()),

    // 1. Hidratação Não-Destrutiva com Replay de Eventos do Usuário
    // Se o usuário clicar em um botão antes do JS carregar, o evento é gravado e reexecutado pós-hidratação!
    provideClientHydration(
      withEventReplay(),
      withHttpTransferCacheOptions({
        includePostRequests: false
      })
    )
  ]
};

2. Componente com Checagem Isomórfica: src/app/components/ssr-safe-widget.component.ts

// src/app/components/ssr-safe-widget.component.ts
import { Component, inject, PLATFORM_ID, signal, OnInit } from '@angular/core';
import { isPlatformBrowser, isPlatformServer, CommonModule } from '@angular/common';

interface ServerRenderStats {
  environment: string;
  renderTimestamp: string;
  viewportDimensions?: string;
}

@Component({
  selector: 'app-ssr-safe-widget',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="max-w-xl mx-auto p-6 bg-white border border-slate-200 rounded-2xl shadow-sm space-y-4">
      <div class="border-b pb-3">
        <h2 class="text-xl font-bold text-slate-800">⚡ Angular SSR & Hydration Universal</h2>
        <p class="text-xs text-slate-500">withEventReplay() e TransferState de dados HTTP</p>
      </div>

      <div class="p-4 bg-slate-50 border rounded-xl space-y-2 text-xs">
        <div class="flex justify-between">
          <span class="text-slate-500 font-semibold">Executando em:</span>
          <span
            class="px-2 py-0.5 rounded-full font-bold uppercase"
            [class.bg-purple-100]="isBrowser()"
            [class.text-purple-800]="isBrowser()"
            [class.bg-blue-100]="!isBrowser()"
            [class.text-blue-800]="!isBrowser()"
          >
            {{ isBrowser() ? 'Navegador (Cliente)' : 'Node.js (Servidor SSR)' }}
          </span>
        </div>

        <div class="flex justify-between">
          <span class="text-slate-500 font-semibold">Timestamp de Renderização:</span>
          <span class="font-mono text-slate-800">{{ stats().renderTimestamp }}</span>
        </div>

        @if (isBrowser()) {
          <div class="flex justify-between pt-1 border-t text-emerald-700 font-semibold">
            <span>Dimensões do Viewport (DOM):</span>
            <span class="font-mono">{{ stats().viewportDimensions }}</span>
          </div>
        }
      </div>

      <button
        type="button"
        (click)="handleEarlyClick()"
        class="w-full py-2.5 bg-red-600 hover:bg-red-700 text-white rounded-xl text-xs font-bold transition-colors"
      >
        Testar Event Replay (Clique durante Carga)
      </button>

      @if (clickConfirmed()) {
        <div class="p-3 bg-emerald-50 text-emerald-800 rounded-lg text-xs font-medium">
          ✓ Evento de clique capturado e processado com sucesso pós-hidratação!
        </div>
      }
    </div>
  `
})
export class SsrSafeWidgetComponent implements OnInit {
  private platformId = inject(PLATFORM_ID);

  isBrowser = signal<boolean>(isPlatformBrowser(this.platformId));
  clickConfirmed = signal<boolean>(false);

  stats = signal<ServerRenderStats>({
    environment: isPlatformServer(this.platformId) ? 'Server' : 'Browser',
    renderTimestamp: new Date().toISOString()
  });

  ngOnInit(): void {
    // Código seguro: só acessa o objeto window se estiver comprovadamente no navegador!
    if (isPlatformBrowser(this.platformId)) {
      this.stats.update((curr) => ({
        ...curr,
        viewportDimensions: `${window.innerWidth} x ${window.innerHeight} px`
      }));
    }
  }

  handleEarlyClick(): void {
    this.clickConfirmed.set(true);
  }
}

🖥️ Saída / Comportamento Esperado

HTML Enviado pelo Servidor Node (Visualizado via View-Source):
<app-ssr-safe-widget>
  ...
  <span>Node.js (Servidor SSR)</span>
  ...
</app-ssr-safe-widget>

No Navegador (Hydration):
O Angular 18 reconcilia o DOM sem recriar os nós HTML.
isBrowser() torna-se true.
O badge atualiza para "Navegador (Cliente)".
As dimensões do Viewport ("1920 x 1080 px") são preenchidas sem nenhum hydration error.

Caso o usuário clique no botão antes do bundle JavaScript terminar de baixar:
O withEventReplay() grava o evento de clique na fila interna e o reproduz imediatamente após a hidratação, ativando a mensagem de confirmação.

🔍 Destaques da Implementação

  1. provideClientHydration(withEventReplay()): Elimina a frustração de “botões mortos” em conexões 3G/4G durante o carregamento inicial.
  2. isPlatformBrowser(this.platformId): A forma canônica e obrigatória de blindar APIs do browser (window, localStorage, navigator) contra falhas no SSR.
  3. TransferState Embutido: O HttpClient serializa as respostas do servidor e as reidrata no cliente sem disparar requisições duplicadas.