Voltar ao blog
Jaime Silva··6 min de leitura·co-escrito com IA

Next.js 16 + React 19: o que aprendi a construir o Descomplicai

Um tour técnico honesto pelo stack do Descomplicai: App Router, Server Components, Partial Pre-rendering, o que funcionou, os gotchas de hydration e streaming, e as decisões que mudaram a performance.


Há uma diferença entre ler documentação sobre uma tecnologia e construir algo real com ela. A documentação mostra o caminho feliz. A realidade mostra os gotchas, as decisões a meio do projecto, e as horas a debuggar um erro que não está na documentação.

O Descomplicai foi construído em Next.js 16 com React 19 desde o início. Este post é o que eu gostaria de ter lido antes de começar.


A decisão de ir full App Router

A primeira decisão estrutural foi usar exclusivamente o App Router. sem pages directory, sem rotas mistas. No início pareceu arriscada porque ainda havia posts de 2024 a desaconselhar para produção. Mas a direcção era clara: o App Router é o futuro do Next.js e o Pages Router está em modo de manutenção.

A estrutura ficou assim:

src/
  app/
    (marketing)/
      page.tsx          # homepage
      about/page.tsx
      blog/
        page.tsx        # lista de posts
        [slug]/page.tsx # post individual
    (app)/
      projetos/page.tsx
      mente/page.tsx
      salao-motivacional/page.tsx
      support/page.tsx
    api/
      contact/route.ts
      newsletter/route.ts
  components/
  lib/

Os route groups (marketing) e (app) permitem layouts diferentes sem afectar os URLs. Foi uma das primeiras coisas que mudou como penso sobre organização de código em Next.js.


Server Components: a mudança mental que demora

React 19 com Server Components foi o ajuste mais difícil. Conceptualmente é simples: o componente corre no servidor, não envia JavaScript para o cliente, pode aceder directamente a bases de dados.

O problema é que a intuição que desenvolves com React ao longo de anos vai contra isso. A primeira semana estava constantemente a adicionar 'use client' a componentes que não precisavam.

A heurística que acabou por funcionar: começa tudo como Server Component e só moves para Client Component quando há uma razão concreta. interactividade, estado local, acesso a browser APIs, event listeners.

// Server Component: corre no servidor, zero JS no cliente
// src/app/blog/[slug]/page.tsx

import { getPostBySlug } from '@/lib/blog'
import { MDXRemote } from 'next-mdx-remote/rsc'

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug) // acesso directo ao filesystem
  
  return (
    <article>
      <h1>{post.title}</h1>
      <MDXRemote source={post.content} />
    </article>
  )
}
// Client Component: necessário para interactividade
// src/components/NewsletterForm.tsx
'use client'

import { useState } from 'react'

export function NewsletterForm() {
  const [email, setEmail] = useState('')
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
  
  // ...handler com estado e eventos
}

Partial Pre-rendering: o que é e quando importa

O PPR foi uma das features que mais expectativa criou no Next.js 15/16. A ideia é elegante: renderizar estaticamente a estrutura da página no build, mas deixar "buracos" dinâmicos que são preenchidos em streaming sem bloquear o HTML inicial.

Para o Descomplicai, o ganho real foi nas páginas que têm conteúdo estático mas precisam de um componente dinâmico. o contador de projectos, dados do utilizador no header se estiver autenticado.

Para activar:

// next.config.ts
const nextConfig = {
  experimental: {
    ppr: true,
  },
}
// A parte estática renderiza no build
// O Suspense boundary marca o "buraco" dinâmico

import { Suspense } from 'react'
import { ProjectCount } from '@/components/ProjectCount'

export default function HomePage() {
  return (
    <main>
      <h1>Descomplicai</h1>
      <p>IA acessível para Portugal.</p>
      
      {/* Este componente é dinâmico: renderiza em streaming */}
      <Suspense fallback={<span>...</span>}>
        <ProjectCount />
      </Suspense>
    </main>
  )
}

O resultado: o HTML estático chega ao browser instantaneamente, o componente dinâmico aparece em streaming.


O gotcha de hydration que me custou horas

Conteúdo que difere entre o HTML renderizado no servidor e o que o React renderiza no cliente causa um erro de hydration. O mais comum é usar Date.now() ou Math.random() directamente num componente, ou aceder a window/localStorage sem guard.

// ERRADO: causa hydration mismatch
function Component() {
  return <div>{new Date().toLocaleDateString('pt-PT')}</div>
}

// CERTO: garante consistência servidor/cliente
'use client'

import { useState, useEffect } from 'react'

function Component() {
  const [dateStr, setDateStr] = useState('')
  
  useEffect(() => {
    setDateStr(new Date().toLocaleDateString('pt-PT'))
  }, [])
  
  if (!dateStr) return null // ou um skeleton
  return <div>{dateStr}</div>
}

Mas a versão ainda melhor é perguntar: este valor precisa mesmo de ser dinâmico no cliente? Se a data de publicação de um post é estática, formata-a no Server Component e acabou.


Streaming e Suspense: a diferença que se sente

Em vez de esperar que toda a página esteja pronta para enviar HTML, o servidor envia o que tem e vai completando. Para o blog, a lista de posts carrega instantaneamente. Para páginas com dados externos, o utilizador vê a estrutura imediatamente e os dados aparecem quando chegam.

// src/app/projetos/page.tsx

import { Suspense } from 'react'
import { ProjectGrid } from '@/components/ProjectGrid'
import { ProjectSkeleton } from '@/components/ProjectSkeleton'

export default function ProjectsPage() {
  return (
    <div>
      <h1>Projectos</h1>
      <Suspense fallback={<ProjectSkeleton />}>
        {/* Este componente faz fetch de dados: chega em streaming */}
        <ProjectGrid />
      </Suspense>
    </div>
  )
}

O skeleton não é só cosmético. Comunica ao utilizador que algo está a chegar e reduz a percepção de lentidão.


Performance: o que fez diferença

Os Core Web Vitals ficaram em boa forma por três razões:

1. Server Components reduzem drasticamente o JavaScript enviado ao cliente. Num site como o Descomplicai, onde grande parte do conteúdo é editorial, isto é significativo.

2. `next/image` com lazy loading por defeito. Todas as imagens optimizadas automaticamente, servidas em WebP, com dimensões correctas. Zero configuração adicional.

3. Font optimisation automática. O Next.js descarrega e serve fontes localmente, eliminando o round-trip para servidores externos.

// Fontes: hosted localmente pelo Next.js, zero round-trip externo
import { Inter, JetBrains_Mono } from 'next/font/google'

const inter = Inter({ 
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
})

const jetbrainsMono = JetBrains_Mono({
  subsets: ['latin'],
  variable: '--font-mono',
  display: 'swap',
})

O que mudaria se recomeçasse

Definiria os boundaries Server/Client Component mais cedo. Passei tempo a refactorizar componentes que cresceram como Client Components desnecessariamente.

Usaria mais `generateStaticParams` para o blog. O conteúdo não muda após publicação. Não há razão para renderizar no servidor a cada pedido.

// Gera todas as páginas de posts estaticamente no build
export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map((post) => ({ slug: post.slug }))
}

Investiria mais cedo em error boundaries. O streaming torna os erros mais localizados, mas também mais silenciosos sem error boundaries bem posicionados.


Vale a pena?

Sim. Sem hesitação.

O App Router tem uma curva de aprendizagem real. especialmente a transição mental para Server Components. Mas uma vez interiorizado, o modelo é mais limpo, mais performativo, e mais alinhado com como a web devia funcionar.

O Descomplicai está em produção no Vercel e funciona bem. O stack técnico completo está documentado em /projetos.


Se estás a migrar de Pages Router ou a começar um projecto novo, o App Router é o caminho. O investimento inicial paga-se rapidamente.

Partilhar:TwitterLinkedIn

Descomplicai

Tens questoes ou um projeto em mente?

Envia uma mensagem: respondo a todas.