Waku: серверные компоненты React без Next.js
Знакомьтесь с Waku — лёгким React-фреймворком для серверных компонентов без Next.js. Создайте пример приложения и изучите маршрутизацию, режимы рендеринга и развёртывание.
Waku — небольшой React-фреймворк на базе Vite, который поддерживает серверные компоненты (server components) и серверные действия (server actions) React 19. Освоить нужно в основном две вещи: файловую маршрутизацию в src/pages и экспорт getConfig на каждой странице, который задаёт статический или динамический рендеринг.
Если вы работали с серверными компонентами только в Next.js App Router, трудно понять, что в них относится к React, а что к Next. Waku сохраняет модель React и отказывается от большей части окружающего её фреймворка. В статье мы создадим одно небольшое приложение, от генерации каркаса до деплоя, чтобы вы могли решить, стоит ли пробовать Waku в пет-проекте. Ветка 1.0 выходит в виде релиз-кандидатов. Версия v1.0.0-rc.2 опубликована 28 сентября 2026 г., поэтому проверьте страницу релизов Waku: возможно, стабильная версия 1.0 уже вышла.
Ключевые выводы
- Waku — минималистичный React-фреймворк на Vite с файловой маршрутизацией в
src/pages. Каждый макет (layout) и каждая страница экспортируютgetConfig, который возвращаетrender: 'static'илиrender: 'dynamic'. - По умолчанию макеты, страницы и слайсы в Waku рендерятся статически. Страница рендерится при каждом запросе, только если её
getConfigвозвращаетrender: 'dynamic'. - Статический маршрут с сегментом, например
src/pages/releases/[slug].tsx, должен возвращать изgetConfigмассивstaticPaths. - По умолчанию Waku деплоится на Node.js. Также поддерживаются полностью статическая сборка, Vercel, Netlify и Cloudflare Workers. Поддержка Deno Deploy, Bun и AWS Lambda помечена как экспериментальная.
- На момент выхода v1.0.0-rc.2 (28 сентября 2026 г.) Waku 1.0 находился на стадии релиз-кандидата. Фреймворк хорошо подходит для небольших проектов, если вы готовы к изменениям, характерным для предрелизных версий.
Как создать React-проект на Waku?
Чтобы создать React-проект на Waku, выполните npm create waku@latest. Дальше вам понадобятся три команды CLI: waku dev, waku build и 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
Согласно документации Waku по началу работы, поддерживаются версии Node.js ^26.0.0, ^24.0.0 и ^22.15.0. Для сборки серверных компонентов Waku использует официальный плагин Vite @vitejs/plugin-rsc, поэтому в конфигурационном файле есть ключ vite для подключения плагинов.
Асинхронная страница с загрузкой данных
В Waku страница — это файл в src/pages, который экспортирует компонент по умолчанию. Этот компонент может быть асинхронным серверным компонентом, который напрямую ожидает данные через await. Если вы знакомы с загрузкой данных в серверных компонентах в Next.js, тело компонента покажется вам знакомым. Отличие только в именованном экспорте getConfig.
Чтобы не зависеть от внешнего API, пример читает локальный JSON-файл. В документации Waku сказано, что файлы из папки private в корне проекта можно безопасно читать из серверных компонентов.
// 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;
};
Поскольку getConfig возвращает render: 'dynamic', страница рендерится на сервере при каждом запросе. Без этой строки она была бы пререндерена во время сборки. Тег <title> работает, потому что Waku поднимает теги title, meta и link в <head> документа.
Добавляем клиентский компонент с помощью ‘use client’
Директива 'use client' в начале файла превращает его в границу между сервером и клиентом, когда этот файл импортирует серверный компонент. Начиная с этой точки, все компоненты, которые подключает файл, проходят гидратацию и выполняются также в браузере. Это та же директива и то же правило, что и в 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>;
};
Клиентские компоненты не могут импортировать серверные. Однако результат серверного рендеринга всё равно можно им передать через children или другой проп:
// 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>
Абзац с описанием рендерится на сервере и передаётся в Collapsible как проп. Клиентский файл его не импортирует. Частая путаница с серверными компонентами React связана с ролью 'use server'. Согласно документации Waku, эта директива помечает серверные действия, а не серверные компоненты, поэтому в начале файла серверного компонента ей не место. Клиентские компоненты тоже рендерятся на сервере в HTML до гидратации. Запись сессии (session replay) такой страницы покажет, реагирует ли кнопка лайка на первый клик или ничего не делает, пока не завершится гидратация.
Маршрутизация: макеты, динамические сегменты и режимы рендеринга
Waku рендерит макеты, страницы и слайсы статически, если не указано иное. С обработчиками API всё наоборот: по умолчанию они динамические. Режим рендеринга задаётся для каждого файла отдельно, так что статический макет может оборачивать динамическую страницу.
src/
pages/
_layout.tsx
index.tsx
releases/
[slug].tsx
components/
like-button.tsx
collapsible.tsx
private/
releases.json
Файл _layout.tsx применяется к своему маршруту и ко всем вложенным в него маршрутам, а его компонент должен принимать проп 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;
};
Файлы с именами в квадратных скобках — это маршруты с сегментами. Значение сегмента приходит как проп, и его можно типизировать с помощью PageProps из waku/router. Если маршрут с сегментом статический, документация Waku по маршрутизации требует, чтобы getConfig возвращал массив staticPaths со значениями для пререндеринга. getConfig может быть асинхронным, поэтому список можно сформировать из данных:
// 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;
};
Когда понадобится, можно подключить слайсы, перехватчики (interceptors) и middleware на Hono, но в нашем небольшом приложении они не используются.
Куда можно задеплоить Waku?
По умолчанию Waku деплоится на Node.js. Также поддерживаются полностью статическая сборка, Vercel, Netlify и Cloudflare Workers. В документации Waku по деплою поддержка Deno Deploy, Bun и AWS Lambda помечена как экспериментальная.
- Node.js:
waku startзапускает продакшен-сервер. Для автономного развёртывания скопируйте папкуdistи выполнитеnode dist/serve-node.js. - Чистый SSG: загрузите
dist/publicна любой статический хостинг. - Vercel:
vercel - Netlify:
NETLIFY=1 npm run build, затемnetlify deploy - Cloudflare Workers:
CLOUDFLARE=1 npm run build, затемwrangler deploy - Deno Deploy, Bun, AWS Lambda (экспериментально): импортируйте
waku/adapters/deno,waku/adapters/bunилиwaku/adapters/aws-lambdaвsrc/waku.server.tsx.
Полностью статическая сборка исключает всё, что требует сервера во время запроса: динамический рендеринг, серверные действия и API-маршруты. Поэтому динамической главной странице из примера выше понадобится render: 'static', прежде чем её можно будет задеплоить как SSG.
Когда Next.js по-прежнему лучше?
Если вам нужен стабильный релиз, надёжнее выбрать Next.js. На момент v1.0.0-rc.2 Waku 1.0 всё ещё был релиз-кандидатом, а в заметках к релизу rc.2 есть пункт «breaking: drop deprecated apis», так что между версиями возможны изменения. Кроме того, у Next.js намного больше экосистема, интеграций с хостингами и обучающих материалов. В нём также больше встроенных возможностей, которые в другом случае пришлось бы собирать самостоятельно. В документации Waku сказано, что выбор зависит от желаемой архитектуры, а не от размера проекта. Waku сохраняет сам фреймворк тонким и полагается на библиотеки экосистемы во всём остальном, тогда как более тяжёлые фреймворки берут большую часть этой работы на себя. Если вы хотите, чтобы такие решения за вас принимал фреймворк, оставайтесь на Next.js.
Заключение
Waku предлагает знакомую по App Router модель серверных компонентов с гораздо более тонким слоем фреймворка вокруг неё: асинхронные страницы, та же граница 'use client', композиция через children и экспорт getConfig, который явно задаёт статический или динамический режим для каждого файла. Следующий шаг — выполнить npm create waku@latest, переписать на Waku один небольшой маршрут из Next.js-проекта и зафиксировать точную версию RC в package.json, чтобы очередной релиз-кандидат незаметно не изменил поведение приложения.
Часто задаваемые вопросы
Как добавить API-маршрут в Waku?
Создайте файл в src/pages/_api и экспортируйте функцию для каждого HTTP-метода, который должен обрабатывать маршрут, например GET, POST, PUT, PATCH или DELETE. Путь маршрута определяется именем файла. Каждый обработчик принимает стандартный Request и возвращает стандартный Response. По умолчанию обработчики API рендерятся динамически, а getConfig, возвращающий render 'static', пререндерит обработчик во время сборки. Это удобно, например, для RSS-ленты.
Как работают переменные окружения в Waku?
Серверный код читает переменные с помощью функции getEnv, импортируемой из 'waku'. В среде Node.js также работает process.env. Клиентские компоненты могут читать только переменные с префиксом WAKU_PUBLIC_ через import.meta.env, например import.meta.env.WAKU_PUBLIC_HELLO. Всё, что имеет этот префикс, попадает в продакшен-бандл JavaScript в открытом виде, поэтому никогда не добавляйте префикс WAKU_PUBLIC_ к API-ключам и другим секретам.
Что использовать для внутренней навигации в Waku: теги a или компонент Link?
Для внутренних ссылок используйте компонент Link, импортируемый из 'waku'. Его проп to принимает либо строку маршрута, либо объект с полями to, params, search и hash. В этом случае навигация выполняется на клиенте через роутер Waku. Обычный тег a вместо этого запускает стандартную браузерную навигацию. Для программной навигации или чтения текущего пути и query-параметров вызывайте хук useRouter из 'waku' внутри клиентского компонента.
Безопасны ли серверные действия Waku по умолчанию?
Нет. Функция с директивой 'use server' становится эндпоинтом, который может вызвать клиент. Документация Waku предупреждает, что эти эндпоинты ничем не защищены, если вы сами не реализуете проверки аутентификации и авторизации внутри функции. Проверяйте личность и права пользователя в каждом серверном действии и добавляйте директиву только к функциям, которые действительно хотите сделать доступными, чтобы не создать непреднамеренные эндпоинты.
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