Skip to content

Fix: Waku Not Working — Server Components Not Rendering, Client Components Not Interactive, or Build Errors

FixDevs ·

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 nothing

Or the dev server shows a blank page:

npx waku dev
# Server starts but browser shows white screen

Or imports between server and client components fail:

Error: Cannot import server-only module from a client component

Why 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 like useState and useEffect throw.
  • The entry point must be configured correctly — Waku uses src/entries.tsx as 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 async and 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles