Waku: React Server Components sin Next.js
Explora Waku, un framework React ligero para usar componentes de servidor de React sin Next.js. Crea una aplicación de ejemplo y conoce el enrutamiento, el renderizado y el despliegue.
Waku es un framework de React pequeño, construido sobre Vite, que ejecuta los server components y las server actions de React 19. Lo principal que hay que aprender es el enrutamiento basado en archivos de src/pages y la exportación getConfig de cada página, que determina si el renderizado es estático o dinámico.
Si todo tu trabajo con server components se ha hecho dentro del App Router de Next.js, es difícil distinguir qué pertenece a React y qué pertenece a Next. Waku conserva el modelo de React y prescinde de la mayor parte del framework que lo rodea. En este artículo se construye una aplicación pequeña, desde el andamiaje inicial hasta el despliegue, para que puedas decidir si vale la pena probarlo en un proyecto personal. La rama 1.0 se publica como release candidates. v1.0.0-rc.2 se publicó el 28 de septiembre de 2026, así que consulta la página de versiones de Waku para comprobar si ya existe una versión 1.0 estable.
Puntos clave
- Waku es un framework minimalista de React sobre Vite, con enrutamiento basado en archivos en
src/pagesy una exportacióngetConfigpor cada layout y página que devuelverender: 'static'orender: 'dynamic'. - En Waku, los layouts, las páginas y los slices se renderizan de forma estática por defecto. Una página solo se renderiza en cada solicitud cuando su
getConfigdevuelverender: 'dynamic'. - Una ruta de segmento estática como
src/pages/releases/[slug].tsxdebe devolver un arraystaticPathsdesdegetConfig. - Waku se despliega en Node.js por defecto y también admite salida puramente estática, Vercel, Netlify y Cloudflare Workers. Deno Deploy, Bun y AWS Lambda están marcados como experimentales.
- Waku 1.0 se encontraba en fase de release candidate con la v1.0.0-rc.2 (28 de septiembre de 2026), por lo que encaja bien en proyectos pequeños en los que aceptes los cambios frecuentes propios de una versión preliminar.
¿Cómo se crea un proyecto de React con Waku?
Para crear un proyecto de React con Waku, ejecuta npm create waku@latest. A partir de ahí se utilizan tres comandos de la CLI: waku dev, waku build y 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
La documentación de primeros pasos de Waku indica que las versiones compatibles de Node.js son ^26.0.0, ^24.0.0 o ^22.15.0. Internamente, Waku utiliza el plugin oficial de Vite @vitejs/plugin-rsc para empaquetar los server components, y por eso su archivo de configuración incluye una clave vite para los plugins.
Una página asíncrona que obtiene datos
En Waku, una página es un archivo dentro de src/pages con un componente exportado por defecto. Ese componente puede ser un server component asíncrono que espera los datos directamente con await. Si conoces la obtención de datos con server components en Next.js, el cuerpo del componente te resultará idéntico. Lo que cambia es la exportación con nombre getConfig.
Para no depender de una API externa, el ejemplo lee un archivo JSON local. La documentación de Waku indica que los archivos de una carpeta private situada en la raíz del proyecto pueden leerse de forma segura desde los 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 getConfig devuelve render: 'dynamic', esta página se renderiza en el servidor en cada solicitud. Sin esa línea, se prerrenderizaría en tiempo de compilación. La etiqueta <title> funciona porque Waku eleva (hoisting) las etiquetas title, meta y link al head del documento.
Cómo añadir un client component con ‘use client’
Colocar 'use client' al principio de un archivo lo convierte en una frontera entre servidor y cliente cuando un server component lo importa. A partir de ese punto, todos los componentes que importa ese archivo se hidratan y se ejecutan también en el navegador. Es la misma directiva y la misma regla que ya usas en el 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>;
};
Los client components no pueden importar server components. Aun así, el resultado renderizado en el servidor puede llegarles si se pasa como children o como otra 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>
El párrafo con el resumen se renderiza en el servidor y se entrega a Collapsible como prop. El archivo de cliente nunca lo importa. Una confusión habitual con los React server components tiene que ver con el papel de 'use server'. Según la documentación de Waku, esa directiva marca server actions, no server components, por lo que no debe colocarse al principio de un archivo de server component. Los client components también se renderizan en el servidor a HTML antes de hidratarse. Una grabación de sesión (session replay) de una página como esta muestra si el botón de “me gusta” responde al primer clic o no hace nada hasta que termina la hidratación.
Enrutamiento: layouts, segmentos dinámicos y modos de renderizado
Waku renderiza los layouts, las páginas y los slices de forma estática, salvo que indiques lo contrario. Los manejadores de API funcionan al revés y son dinámicos por defecto. El modo de renderizado se define por archivo, de modo que un layout estático puede envolver una página dinámica.
src/
pages/
_layout.tsx
index.tsx
releases/
[slug].tsx
components/
like-button.tsx
collapsible.tsx
private/
releases.json
Un archivo _layout.tsx se aplica a su propia ruta y a todas las rutas anidadas bajo ella, y su componente debe recibir una 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;
};
Los archivos con corchetes son rutas de segmento. El valor del segmento llega como prop, y puedes tiparlo con PageProps de waku/router. Cuando una ruta de segmento es estática, la documentación de enrutamiento de Waku exige que getConfig devuelva un array staticPaths con los valores que se deben prerrenderizar. getConfig puede ser asíncrona, así que la lista puede generarse a partir de datos:
// 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;
};
Los slices, los interceptores y el middleware de Hono también están disponibles cuando los necesites, pero esta pequeña aplicación no utiliza ninguno de ellos.
¿Dónde se puede desplegar Waku?
Waku se despliega en Node.js por defecto. También admite salida puramente estática, Vercel, Netlify y Cloudflare Workers. La documentación de despliegue de Waku marca Deno Deploy, Bun y AWS Lambda como experimentales.
- Node.js:
waku startejecuta el servidor de producción. Para una copia independiente, distribuye la carpetadisty ejecutanode dist/serve-node.js. - SSG puro: sube
dist/publica cualquier hosting estático. - Vercel:
vercel - Netlify:
NETLIFY=1 npm run buildy, a continuación,netlify deploy - Cloudflare Workers:
CLOUDFLARE=1 npm run buildy, a continuación,wrangler deploy - Deno Deploy, Bun, AWS Lambda (experimentales): importa
waku/adapters/deno,waku/adapters/bunowaku/adapters/aws-lambdaensrc/waku.server.tsx.
La salida puramente estática elimina todo lo que requiere un servidor en tiempo de solicitud: el renderizado dinámico, las server actions y las rutas de API. Por tanto, la página de inicio dinámica del ejemplo anterior necesitaría render: 'static' para poder desplegarse como SSG.
¿Cuándo sigue siendo Next.js la mejor opción?
Next.js es la opción más segura si necesitas una versión estable. Waku 1.0 seguía siendo una release candidate en la v1.0.0-rc.2, y las notas de esa versión incluyen “breaking: drop deprecated apis”, así que conviene esperar cambios entre versiones. Next.js cuenta además con un ecosistema mucho más amplio, más integraciones de hosting y más tutoriales. También incorpora de serie más funcionalidades que, de otro modo, tendrías que montar tú mismo. La documentación de Waku señala que la elección depende de la arquitectura que quieras, no del tamaño del proyecto. Waku mantiene el framework en sí lo más ligero posible y delega el resto en bibliotecas del ecosistema, mientras que los frameworks más pesados asumen una mayor parte de ese trabajo. Si prefieres que el framework tome esas decisiones por ti, quédate con Next.js.
Conclusión
Waku te ofrece el modelo de server components que ya conoces del App Router, pero con mucho menos framework alrededor: páginas asíncronas, la misma frontera 'use client', composición mediante children y una exportación getConfig que hace explícita, archivo por archivo, la elección entre renderizado estático y dinámico. El siguiente paso es ejecutar npm create waku@latest, reconstruir con él una ruta pequeña de Next.js y fijar la versión RC exacta en package.json para que una futura release candidate no cambie el comportamiento de tu aplicación sin que te des cuenta.
Preguntas frecuentes
¿Cómo se añade una ruta de API en Waku?
Añade un archivo en src/pages/_api y exporta una función por cada método HTTP que deba gestionar la ruta, como GET, POST, PUT, PATCH o DELETE. La ruta se obtiene del nombre del archivo. Cada manejador recibe un Request estándar y devuelve un Response estándar. Los manejadores de API se renderizan de forma dinámica por defecto, y un getConfig que devuelva render 'static' prerrenderiza el manejador en tiempo de compilación, lo que resulta útil para salidas como un feed RSS.
¿Cómo funcionan las variables de entorno en Waku?
El código de servidor lee las variables con la función getEnv importada de 'waku', y process.env también funciona en entornos Node.js. Los client components solo pueden leer variables con el prefijo WAKU_PUBLIC_, a través de import.meta.env, por ejemplo import.meta.env.WAKU_PUBLIC_HELLO. Todo lo que lleve ese prefijo se incluye como texto plano en el bundle de JavaScript de producción, así que nunca uses el prefijo WAKU_PUBLIC_ en una clave de API ni en ningún otro secreto.
¿Debo usar etiquetas de anclaje o el componente Link para la navegación interna en Waku?
Usa el componente Link importado de 'waku' para los enlaces internos. Su prop to acepta una cadena con la ruta o un objeto estructurado con to, params, search y hash, y la navegación se realiza en el cliente mediante el router de Waku. Una etiqueta de anclaje normal, en cambio, provoca una navegación estándar del navegador. Para navegar de forma programática o leer la ruta y los parámetros de consulta actuales, llama al hook useRouter de 'waku' dentro de un client component.
¿Las server actions de Waku son seguras por defecto?
No. Una función marcada con 'use server' se convierte en un endpoint que el cliente puede invocar, y la documentación de Waku advierte que nada protege estos endpoints a menos que escribas comprobaciones de autenticación y autorización dentro de la propia función. Verifica la identidad y los permisos del usuario en cada server action y añade la directiva solo a las funciones que realmente quieras exponer, para no crear endpoints de forma involuntaria.
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