⛩️

React FrameworkのWakuを触ってみてワクワクした話⛩️

2025/01/09に公開

こんにちは!cordeliaです。

2024年12月、React TokyoというReactの世界を広げることを目的としたコミュニティが誕生しました。その窓口としてのWebサイトはWaku⛩️というフレームワークで作られています。幸運なことにそのサイト開発に私も参加させていただいたんですね☺️ そこでWakuを初めて触ったので、どのようなものか皆様に紹介します。

Waku⛩️とは

WakuはReactを使ったミニマルなフレームワークです。小規模から中規模なプロジェクトを素早く開発できるように設計されており、Jotai Zustand Valtio といった状態管理ライブラリの作者である@dai_shiさんによって開発されています。

https://waku.gg/

そして先日アップデートされたばかり。めでたい🎉

https://x.com/dai_shi/status/1874454115281260926


React Server Componentsに対応

まずは何といってもこれですね。React Server Components(以下RSC)とはコンポーネントの新しいレンダリング方法です。
簡単に説明するとサーバー上で実行される非同期コンポーネントです。HTMLを構築する前段階のコード(例えばデータフェッチやシンタックスハイライトなどのクライアントに不要な処理)はサーバー上でのみ実行され、バンドルされません。その為クライアントへのデータ送信量を減らすことができます。詳しくはドキュメントをご覧ください。

https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#sharing-code-between-server-and-client

https://www.joshwcomeau.com/react/server-components/


RSCにおいてコンポーネントはデフォルトでserver componentになり、Wakuも同じです。client componentにしたい場合はファイル先頭にuse clientを書くのは変わりません。以下はserver compoentでコードのシンタックスハイライトを行う例です。brightというRSCに対応しているライブラリを使っています。

import { Code } from "bright";
import { getArticleBySlug } from "../lib";

export default async function BlogArticlePage({ slug }) {
  const article = await getArticleBySlug(slug);

  return (
    <>
      <h1>{article.title}</h1>
      <Code lang="ts" title="sample.ts">
        {article.code}
      </Code>
    </>
  );
};


もちろんServer Functions(旧Server Actions)も使えます。この場合はuse serverを書いてください。以下は<form>からデータを受け取って処理するFunctionsの例です。

"use server";

export const postData = async (formdata: FormData) => {
  const data = formdata.get("message");

  const response = await fetch("example.com/v1/posts", {
    method: "POST",
    headers: {
      "Content-type": "application/json",
    },
    body: JSON.stringify(data),
  });
};
'use client';

import { postData } from '../actions/postData';

export const ContactForm = () => {
  return (
    <form action={postData}>
      <input type="text" name="message" />
      <button type="submit">送信</button>
    </form>
  );
};


型安全なルーティング

Wakuはファイルベースルーティングであり、さらに型安全なルーティングを行うことができるのです!!

まずは新しくプロジェクトを作成しましょう。順を追って説明していきます。

npm create waku@latest

プロジェクト名を聞かれるので入力してください。

cd my-waku-waku
npm run dev


ページコンポーネントは./src/pagesディレクトリに配置していきます。./src/pages/index.tsxを見てみると以下のようになっています。

./src/pages/index.tsx
// 一部省略
import { Link } from 'waku';
import { getData } from '../lib';

export default async function HomePage() {
  const data = await getData();

  return (
    <div>
      <h1>{data.headline}</h1>
      <p>{data.body}</p>
      <Link to="/about">
        About page
      </Link>
    </div>
  );
}

export const getConfig = async () => {
  return {
    render: 'static',
  } as const;
};

getConfig()はページコンポーネントのレンダリングタイプやその他オプションをexportする非同期関数です。レンダリングタイプは以下2つに対応しています。

  • 'static': Static prerendering(SSG)
  • 'dynamic': Server side rendering(SSR)

この関数を省略した場合は自動的にdynamicになります。

次に./src/pages.gen.tsです。これはルーティングの型定義ファイルで、ページコンポーネントが追加されると自動的に生成されます。

./src/pages.gen.ts
// 一部省略
import type { PathsForPages, GetConfigResponse } from 'waku/router';
import type { getConfig as Index_getConfig } from './pages/index';

type Page =
  | ({ path: "/" } & GetConfigResponse<typeof Index_getConfig>);

declare module "waku/router" {
  interface RouteConfig {
    paths: PathsForPages<Page>;
  }
  interface CreatePagesConfig {
    pages: Page;
  }
}

waku/routerはルーティングを管理するモジュールです。pages.gen.tsの役割はページコンポーネントのgetConfig()でexportされた設定を読み込み、さらにページのpathを定義しwaku/routerに渡して拡張します。これにより型安全にルーティングが行えるんですね。試しに別のページコンポーネントを追加してみてください。pages.gen.tsが変更されているはずです。

./src/pages/about.tsxのようにすればシングルルートになり、./src/pages/blog/[slug].tsxのようにすればセグメントルートにも対応できます。以下はよくあるブログ一覧とブログ詳細ページの例です。

.src/pages/blog/index.tsx
import { Link } from "waku";
import { getAllArticles } from "../lib"

// ブログ一覧ページ
export default async function BlogIndexPage() {
  const articles = await getAllArticles();

  return (
    <div>
      <h1>ブログ一覧</h1>
      <ul>
        {articles.map((article) => (
          <li key={article.id}>
            <Link to={`/blog/${article.id}`}>{article.title}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

export const getConfig = async () => {
  return {
    render: "static",
  } as const;
};
./src/pages/blog/[slug].tsx
import { getArticleBySlug, getStaticPaths } from "../lib"

// ブログ詳細ページ
export default async function BlogArticle({ slug }) {
  const article = await getArticleBySlug(slug);

  return (
    <>
      <h1>{article.title}</h1>
      <p>{article.content}</p>
    </>
  );
}

export const getConfig = async () => {
  const staticPaths = await getStaticPaths();

  return {
    render: "static",
    staticPaths, // ['article1', 'article2',,, ]
  };
};

SSGの場合はgetConfig()staticPathsとして全てのpathの配列を返す必要があります。
他にもネストセグメントワイルドカードセグメントにも対応しています。


Root layoutとRoot element

_layout.tsxはRoot layoutの役割を担うファイルです。./src/pages/_layout.tsxでアプリケーション全体をラップします。グローバルスタイルもここからimportします。ちなみにCSSスタイリングはTailwindがデフォルトになっています。グローバルなコンポーネントやデータはこのファイルに書きます。

./src/pages/_layout.tsx
// 一部省略
import '../styles.css';

import type { ReactNode } from 'react';

import { Providers } from '../components/providers';
import { Header } from '../components/header';
import { Footer } from '../components/footer';
import { getData } from '../lib';

type RootLayoutProps = { children: ReactNode };

export default async function RootLayout({ children }: RootLayoutProps) {
  const data = await getData();

  return (
    <Providers>
      <meta name="description" content={data.description} />
      <link rel="icon" type="image/png" href={data.icon} />
      <Header />
      <main className="m-6 flex items-center *:min-h-64 *:min-w-64 lg:m-0 lg:min-h-svh lg:justify-center">
        {children}
      </main>
      <Footer />
    </Providers>
  );
}

export const getConfig = async () => {
  return {
    render: 'static',
  } as const;
};

ネストしたい場合は./src/pages/blog/_layout.tsxとします。この場合はblogディレクトリ以下全てのページをラップします。

Root elementの<html> <head> <body>をカスタマイズしたい場合は_root.tsxです。

./src/pages/_root.tsx
type Props = { children: ReactNode };

export default async function RootElement({ children }: Props) {
  return (
    <html lang="ja">
      <head></head>
      <body>{children}</body>
    </html>
  );
}

export const getConfig = async () => {
  return {
    render: 'static',
  };
};


リソースについて

ロゴやファビコンなどは./publicへ格納します。

export const Logo = () => {
  // ./public/images/logo.pngの場合
  return <img src="/images/logo.png" alt="logo" />;
};

プロジェクト内から読み込みたいファイルがあるけどクライアントから直接アクセスされたくない、といった場合は./privateに格納すればserver componentから安全にアクセスできます。

export const ServerComponent = async () {
  const file = JSON.parse(readFileSync('./private/data.json', 'utf8'));
  return <OtherComponent>{file}</OtherComponent>;
}


環境変数

全ての環境変数はデフォルトでプライベートとみなされ、server componentからのみアクセスできます。使う場合はgetEnv()です。

import { getEnv } from "waku";

export const ServerComponent = async () => {
  const apiKey = getEnv("API_KEY");

  // 何か処理が続く
};

client componentからアクセスしたい場合は、環境変数の先頭にWAKU_PUBLIC_を付け、以下のようにします。ただしこの値はクライアントに公開されるので注意してください。(上記のプライベート環境変数でも同様の注意が必要です。)

"use client";

export const ClientComponent = () => {
  const publicValue = import.meta.env.WAKU_PUBLIC_VALUE;

  // 何か処理が続く
};


デプロイ

現在はVercelNetlifyがデプロイ先として推奨されています。experimentalですがCloudflareAWS Lamdbaにも可能です。詳しくはこちらを参照してください。


終わりに

Wakuを使ってみた感想は非常にシンプルで使いやすいということです。ドキュメントもシンプルにまとまっています。しかしその上でRSCの対応標準で型安全なルーティングができて凄いなと感じました。以下を表していると思います。

As the minimal React framework, it’s designed to accelerate the work of developers at startups and agencies building small to medium-sized React projects.

最小限のReactフレームワークとして、小中規模のReactプロジェクトを構築する新興企業や代理店の開発者の作業を加速させるように設計されている。

まだ紹介しきれていないこともあるので、興味を持ってくださった方は是非ドキュメントをご覧になりWaku⛩️を使ってみてください。そしてReact Tokyoも覗いてみてください。喜びます🎉

以上、最後まで読んでいただきありがとうございました🙇


https://waku.gg/

https://react-tokyo.vercel.app/

React Tokyo

Discussion