Waku: React Server Components sem Next.js
Conheça o Waku, um framework React leve para usar componentes de servidor sem Next.js. Crie um app de exemplo e veja como funcionam rotas, renderização e implantação.
O Waku é um framework React compacto, construído sobre o Vite, que executa os server components e as server actions do React 19. A maior parte do que você precisa aprender se resume ao roteamento baseado em arquivos em src/pages e a uma exportação getConfig em cada página, que define se a renderização será estática ou dinâmica.
Se todo o seu trabalho com server components aconteceu dentro do App Router do Next.js, é difícil saber o que pertence ao React e o que pertence ao Next. O Waku mantém o modelo do React e dispensa boa parte do framework ao redor dele. Este artigo constrói uma pequena aplicação, do scaffold ao deploy, para que você decida se vale a pena experimentá-lo em um projeto paralelo. A linha 1.0 está sendo distribuída como release candidates. A v1.0.0-rc.2 foi publicada em 28 de setembro de 2026. Consulte a página de releases do Waku para verificar se já saiu uma versão 1.0 estável.
Principais Conclusões
- O Waku é um framework React minimalista sobre o Vite, com roteamento baseado em arquivos em
src/pagese uma exportaçãogetConfigpor layout e por página, que retornarender: 'static'ourender: 'dynamic'. - No Waku, layouts, páginas e slices são renderizados estaticamente por padrão. Uma página só é renderizada a cada requisição quando seu
getConfigretornarender: 'dynamic'. - Uma rota de segmento estática, como
src/pages/releases/[slug].tsx, precisa retornar um arraystaticPathsa partir dogetConfig. - O Waku faz deploy para Node.js por padrão e também oferece suporte a saída puramente estática, Vercel, Netlify e Cloudflare Workers. Deno Deploy, Bun e AWS Lambda estão marcados como experimentais.
- O Waku 1.0 estava em fase de release candidate com a v1.0.0-rc.2 (28 de setembro de 2026). Por isso, ele é uma boa escolha para projetos pequenos em que você aceita as mudanças frequentes de uma versão pré-lançamento.
Como criar um projeto React com Waku?
Para criar um projeto React com Waku, execute npm create waku@latest. A partir daí, você usa três comandos da CLI: waku dev, waku build e waku start.
npm create waku@latest
# then, inside the project:
npx waku dev # local dev server
npx waku build # production build
npx waku start # serve the production build
A documentação de primeiros passos do Waku lista as versões do Node.js suportadas como ^26.0.0, ^24.0.0 ou ^22.15.0. Internamente, o Waku usa o plugin oficial do Vite, @vitejs/plugin-rsc, para fazer o bundle dos server components. É por isso que o arquivo de configuração tem uma chave vite para plugins.
Uma página assíncrona que busca dados
No Waku, uma página é um arquivo em src/pages com um componente exportado como default. Esse componente pode ser um server component assíncrono que aguarda os dados diretamente com await. Se você conhece a busca de dados com server components do Next.js, o corpo do componente vai parecer idêntico. O que muda é a exportação nomeada getConfig.
Para não depender de uma API externa, o exemplo lê um arquivo JSON local. A documentação do Waku informa que arquivos em uma pasta private na raiz do projeto podem ser lidos com segurança a partir de server components.
// private/releases.json
[
{ "slug": "v2-0", "version": "2.0.0", "summary": "New plugin API." },
{ "slug": "v1-9", "version": "1.9.0", "summary": "Bug fixes." }
]
// src/pages/index.tsx
import { readFile } from 'node:fs/promises';
type Release = { slug: string; version: string; summary: string };
export default async function HomePage() {
const releases: Release[] = JSON.parse(
await readFile('./private/releases.json', 'utf8'),
);
return (
<>
<title>Release notes</title>
<h1>Release notes</h1>
<ul>
{releases.map((release) => (
<li key={release.slug}>
<a href={`/releases/${release.slug}`}>{release.version}</a>
</li>
))}
</ul>
</>
);
}
export const getConfig = async () => {
return { render: 'dynamic' } as const;
};
Como o getConfig retorna render: 'dynamic', esta página é renderizada no servidor a cada requisição. Sem essa linha, ela seria pré-renderizada em tempo de build. A tag <title> funciona porque o Waku move as tags title, meta e link para o head do documento.
Adicionando um client component com ‘use client’
Colocar 'use client' no topo de um arquivo o transforma em uma fronteira entre servidor e cliente quando um server component o importa. A partir desse ponto, todo componente que o arquivo importa é hidratado e também executado no navegador. É a mesma diretiva e a mesma regra que você já usa no App Router.
// src/components/like-button.tsx
'use client';
import { useState } from 'react';
export const LikeButton = () => {
const [likes, setLikes] = useState(0);
return <button onClick={() => setLikes((n) => n + 1)}>👍 {likes}</button>;
};
Client components não podem importar server components. Ainda assim, a saída do servidor pode chegar até eles se você a passar como children ou como outra prop:
// src/components/collapsible.tsx
'use client';
import { useState, type ReactNode } from 'react';
export const Collapsible = ({ children }: { children: ReactNode }) => {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen((o) => !o)}>{open ? 'Hide' : 'Details'}</button>
{open && children}
</div>
);
};
// src/pages/index.tsx (inside the map, with both components imported)
<li key={release.slug}>
<a href={`/releases/${release.slug}`}>{release.version}</a>
<LikeButton />
<Collapsible>
<p>{release.summary}</p>
</Collapsible>
</li>
O parágrafo de resumo é renderizado no servidor e entregue ao Collapsible como prop. O arquivo de cliente nunca o importa. Uma confusão comum com React server components envolve o papel do 'use server'. Na documentação do Waku, essa diretiva marca server actions, e não server components. Portanto, ela não deve ficar no topo de um arquivo de server component. Client components continuam sendo renderizados no servidor como HTML antes da hidratação. Um session replay de uma página como esta mostra se o botão de curtir responde ao primeiro clique ou se não faz nada até a hidratação terminar.
Roteamento: layouts, segmentos dinâmicos e modos de renderização
O Waku renderiza layouts, páginas e slices de forma estática, a menos que você indique o contrário. Os handlers de API funcionam ao contrário e são dinâmicos por padrão. O modo de renderização é definido por arquivo, então um layout estático pode envolver uma página dinâmica.
src/
pages/
_layout.tsx
index.tsx
releases/
[slug].tsx
components/
like-button.tsx
collapsible.tsx
private/
releases.json
Um arquivo _layout.tsx se aplica à sua própria rota e a todas as rotas aninhadas abaixo dela. Seu componente precisa receber uma prop children:
// src/pages/_layout.tsx
import type { ReactNode } from 'react';
export default async function RootLayout({ children }: { children: ReactNode }) {
return (
<>
<header><a href="/">Release notes</a></header>
<main>{children}</main>
</>
);
}
export const getConfig = async () => {
return { render: 'static' } as const;
};
Arquivos com colchetes são rotas de segmento. O valor do segmento chega como prop, e você pode tipá-lo com PageProps de waku/router. Quando uma rota de segmento é estática, a documentação de roteamento do Waku exige que o getConfig retorne um array staticPaths com os valores a serem pré-renderizados. Como o getConfig pode ser assíncrono, essa lista pode vir dos dados:
// src/pages/releases/[slug].tsx
import { readFile } from 'node:fs/promises';
import type { PageProps } from 'waku/router';
type Release = { slug: string; version: string; summary: string };
const loadReleases = async (): Promise<Release[]> =>
JSON.parse(await readFile('./private/releases.json', 'utf8'));
export default async function ReleasePage({ slug }: PageProps<'/releases/[slug]'>) {
const release = (await loadReleases()).find((r) => r.slug === slug);
if (!release) return <p>Release not found.</p>;
return (
<>
<title>{`Release ${release.version}`}</title>
<h1>{release.version}</h1>
<p>{release.summary}</p>
</>
);
}
export const getConfig = async () => {
const releases = await loadReleases();
return { render: 'static', staticPaths: releases.map((r) => r.slug) } as const;
};
Slices, interceptors e middlewares do Hono também estão disponíveis quando você precisar deles, mas esta pequena aplicação não usa nenhum desses recursos.
Onde é possível fazer deploy do Waku?
O Waku faz deploy para Node.js por padrão. Ele também oferece suporte a saída puramente estática, Vercel, Netlify e Cloudflare Workers. A documentação de deploy do Waku marca Deno Deploy, Bun e AWS Lambda como experimentais.
- Node.js:
waku startexecuta o servidor de produção. Para uma cópia standalone, distribua a pastadiste executenode dist/serve-node.js. - SSG puro: envie
dist/publicpara qualquer host estático. - Vercel:
vercel - Netlify:
NETLIFY=1 npm run builde, em seguida,netlify deploy - Cloudflare Workers:
CLOUDFLARE=1 npm run builde, em seguida,wrangler deploy - Deno Deploy, Bun, AWS Lambda (experimentais): importe
waku/adapters/deno,waku/adapters/bunouwaku/adapters/aws-lambdaemsrc/waku.server.tsx.
A saída puramente estática elimina tudo o que exige um servidor em tempo de requisição: renderização dinâmica, server actions e rotas de API. Por isso, a página inicial dinâmica mostrada acima precisaria usar render: 'static' antes de poder ser publicada como SSG.
Quando o Next.js ainda é a melhor escolha?
O Next.js é a opção mais segura se você precisa de uma versão estável. O Waku 1.0 ainda era um release candidate na v1.0.0-rc.2, e as notas de lançamento da rc.2 incluem “breaking: drop deprecated apis”. Portanto, espere mudanças entre versões. O Next.js também tem um ecossistema muito maior, mais integrações com plataformas de hospedagem e mais tutoriais. Ele também traz mais recursos nativos, que de outra forma você teria de montar por conta própria. A documentação do Waku afirma que a escolha depende da arquitetura que você deseja, e não do tamanho do projeto. O Waku mantém o framework enxuto e recorre a bibliotecas do ecossistema para o restante, enquanto frameworks mais robustos assumem boa parte desse trabalho. Se você prefere que o framework tome essas decisões por você, continue com o Next.js.
Conclusão
O Waku oferece o modelo de server components que você conhece do App Router com muito menos framework ao redor: páginas assíncronas, a mesma fronteira 'use client', composição via children e uma exportação getConfig que torna explícita, arquivo por arquivo, a escolha entre renderização estática ou dinâmica. O próximo passo é executar npm create waku@latest, reconstruir com ele uma pequena rota do Next.js e fixar a versão exata do RC no package.json, para que um futuro release candidate não altere o comportamento da aplicação sem você perceber.
Perguntas Frequentes
Como adicionar uma rota de API no Waku?
Adicione um arquivo em src/pages/_api e exporte uma função para cada método HTTP que a rota deve tratar, como GET, POST, PUT, PATCH ou DELETE. A rota é derivada do nome do arquivo. Cada handler recebe um Request padrão e retorna um Response padrão. Os handlers de API são renderizados dinamicamente por padrão, e um getConfig que retorna render 'static' pré-renderiza o handler em tempo de build, o que é ideal para saídas como um feed RSS.
Como funcionam as variáveis de ambiente no Waku?
O código do servidor lê variáveis com a função getEnv importada de 'waku', e process.env também funciona em ambientes Node.js. Client components só podem ler variáveis com o prefixo WAKU_PUBLIC_, por meio de import.meta.env, por exemplo import.meta.env.WAKU_PUBLIC_HELLO. Tudo o que tem esse prefixo é incluído como texto puro no bundle JavaScript de produção, portanto nunca use o prefixo WAKU_PUBLIC_ em uma chave de API ou em qualquer outro segredo.
Devo usar tags anchor ou o componente Link para a navegação interna no Waku?
Use o componente Link importado de 'waku' para links internos. Sua prop to aceita uma string de rota ou um objeto estruturado com to, params, search e hash, e a navegação passa a acontecer no cliente por meio do roteador do Waku. Uma tag anchor comum faz uma navegação normal do navegador. Para navegação programática ou para ler o caminho e a query atuais, chame o hook useRouter de 'waku' dentro de um client component.
As server actions do Waku são seguras por padrão?
Não. Uma função marcada com 'use server' se torna um endpoint que o cliente pode chamar, e a documentação do Waku alerta que nada protege esses endpoints, a menos que você escreva verificações de autenticação e autorização dentro da própria função. Verifique a identidade e as permissões do usuário em cada server action e adicione a diretiva apenas às funções que você realmente pretende expor, para não criar endpoints sem querer.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k