Fix: Waku Not Working — Server Components Not Rendering, Client Components Not Interactive, or Build Errors
Quick Answer
How to fix Waku React framework issues — RSC setup, server and client component patterns, data fetching, routing, layout system, and deployment configuration.
The Problem
Server Components render on the server but client components aren’t interactive:
// src/pages/index.tsx
'use client';
export default function HomePage() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}
// Button renders but clicking does nothingOr the dev server shows a blank page:
npx waku dev
# Server starts but browser shows white screenOr imports between server and client components fail:
Error: Cannot import server-only module from a client componentWhy This Happens
Waku is a minimal React framework focused on React Server Components (RSC). It uses a different mental model than Next.js:
- Waku is RSC-first — all components are Server Components by default. Client Components need an explicit
'use client'directive at the top of the file. Without it, hooks likeuseStateanduseEffectthrow. - The entry point must be configured correctly — Waku uses
src/entries.tsxas its main entry. This file defines the server-side rendering setup and root component. - Routing is file-based under
src/pages/— pages are RSC by default. They can import Client Components but can’t use hooks themselves. - Data fetching happens in Server Components — Server Components can be
asyncand fetch data directly. Client Components must receive data via props or use client-side fetching.
Fix 1: Project Setup
npm create waku@latest my-app
cd my-app
npm install
npm run dev// src/entries.tsx — application entry
import { createPages } from 'waku';
export default createPages(async ({ createPage, createLayout }) => {
// Root layout
createLayout({
render: 'static',
path: '/',
component: RootLayout,
});
// Pages
createPage({
render: 'static',
path: '/',
component: HomePage,
});
createPage({
render: 'dynamic',
path: '/posts',
component: PostsPage,
});
createPage({
render: 'dynamic',
path: '/posts/[slug]',
component: PostPage,
});
});// src/components/RootLayout.tsx — Server Component (default)
import type { ReactNode } from 'react';
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My Waku App</title>
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/posts">Posts</a>
</nav>
</header>
<main>{children}</main>
</body>
</html>
);
}Fix 2: Server Components (Data Fetching)
// src/pages/posts.tsx — Server Component (async)
// No 'use client' → runs on server → can fetch data directly
export default async function PostsPage() {
// Fetch data directly in the component — no useEffect needed
const posts = await db.query.posts.findMany({
where: eq(posts.published, true),
orderBy: desc(posts.createdAt),
});
return (
<div>
<h1>Blog Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<a href={`/posts/${post.slug}`}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<time>{new Date(post.createdAt).toLocaleDateString()}</time>
</a>
</li>
))}
</ul>
</div>
);
}
// src/pages/posts/[slug].tsx — dynamic Server Component
export default async function PostPage({ slug }: { slug: string }) {
const post = await db.query.posts.findFirst({
where: eq(posts.slug, slug),
});
if (!post) {
return <div>Post not found</div>;
}
return (
<article>
<h1>{post.title}</h1>
<time>{new Date(post.createdAt).toLocaleDateString()}</time>
<div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
{/* Include interactive Client Component */}
<LikeButton postId={post.id} initialLikes={post.likes} />
</article>
);
}Fix 3: Client Components (Interactivity)
// src/components/LikeButton.tsx — Client Component
'use client'; // Required for hooks and interactivity
import { useState } from 'react';
export function LikeButton({ postId, initialLikes }: {
postId: string;
initialLikes: number;
}) {
const [likes, setLikes] = useState(initialLikes);
const [liked, setLiked] = useState(false);
async function handleLike() {
setLiked(!liked);
setLikes(prev => liked ? prev - 1 : prev + 1);
await fetch(`/api/posts/${postId}/like`, {
method: liked ? 'DELETE' : 'POST',
});
}
return (
<button onClick={handleLike}>
{liked ? '❤️' : '🤍'} {likes}
</button>
);
}
// src/components/SearchBar.tsx — Client Component
'use client';
import { useState, useEffect } from 'react';
export function SearchBar() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
if (query.length < 2) {
setResults([]);
return;
}
const timer = setTimeout(async () => {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await res.json();
setResults(data.results);
}, 300);
return () => clearTimeout(timer);
}, [query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{results.length > 0 && (
<ul>
{results.map(result => (
<li key={result.id}>
<a href={result.url}>{result.title}</a>
</li>
))}
</ul>
)}
</div>
);
}Fix 4: Static vs Dynamic Rendering
// src/entries.tsx
import { createPages } from 'waku';
export default createPages(async ({ createPage, createLayout }) => {
createLayout({
render: 'static',
path: '/',
component: RootLayout,
});
// Static page — pre-rendered at build time
createPage({
render: 'static',
path: '/',
component: HomePage,
});
// Static pages with dynamic paths
const posts = await db.query.posts.findMany();
for (const post of posts) {
createPage({
render: 'static',
path: `/posts/${post.slug}`,
component: PostPage,
});
}
// Dynamic page — rendered on each request (SSR)
createPage({
render: 'dynamic',
path: '/dashboard',
component: DashboardPage,
});
// Dynamic page with params
createPage({
render: 'dynamic',
path: '/users/[id]',
component: UserPage,
});
});Fix 5: API Routes
// Waku doesn't have built-in API routes like Next.js
// Use the server component pattern or a separate API server
// Option 1: Server Actions in Server Components
// src/components/ContactForm.tsx
'use client';
export function ContactForm() {
async function handleSubmit(formData: FormData) {
const res = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
}),
});
if (res.ok) {
alert('Message sent!');
}
}
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(new FormData(e.currentTarget)); }}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
);
}Fix 6: Styling
// Waku supports CSS imports and CSS Modules
// Global CSS — import in root layout
// src/components/RootLayout.tsx
import '../styles/global.css';
// CSS Modules
// src/components/Card.module.css
/*
.card { padding: 16px; border-radius: 8px; border: 1px solid #eee; }
.title { font-size: 18px; font-weight: bold; }
*/
import styles from './Card.module.css';
function Card({ title, children }) {
return (
<div className={styles.card}>
<h3 className={styles.title}>{title}</h3>
{children}
</div>
);
}
// Tailwind CSS — configure as usual
// tailwind.config.js content: ['./src/**/*.{ts,tsx}']Still Not Working?
White screen on dev — check that src/entries.tsx exists and exports a default createPages function. This is Waku’s entry point. Without it, no pages are registered and the app renders nothing.
useState throws “not a function” — the component is a Server Component (no 'use client' directive). Add 'use client' at the top of any file that uses hooks, event handlers, or browser APIs.
Dynamic params are undefined — ensure the page path in createPage uses bracket syntax: path: '/posts/[slug]'. The param is passed as a prop to the component: function PostPage({ slug }: { slug: string }).
Build output is empty — static pages are pre-rendered at build time. If createPage calls are inside async conditions that fail, no pages are generated. Check for errors in your data fetching within entries.tsx.
For related React framework issues, see Fix: TanStack Start Not Working and Fix: Next.js App Router Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: TanStack Start Not Working — Server Functions Failing, Routes Not Loading, or SSR Errors
How to fix TanStack Start issues — project setup, file-based routing, server functions with createServerFn, data loading, middleware, SSR hydration, and deployment configuration.
Fix: Wasp Not Working — Build Failing, Auth Not Working, or Operations Returning Empty
How to fix Wasp full-stack framework issues — main.wasp configuration, queries and actions, authentication setup, Prisma integration, job scheduling, and deployment.
Fix: Next.js Server Action Not Working — Action Not Called or Returns Error
How to fix Next.js Server Actions — use server directive, form binding, revalidation, error handling, middleware conflicts, and client component limitations.
Fix: Analog Not Working — Routes Not Loading, API Endpoints Failing, or Vite Build Errors
How to fix Analog (Angular meta-framework) issues — file-based routing, API routes with Nitro, content collections, server-side rendering, markdown pages, and deployment.