React FrameworkのWakuを触ってみてワクワクした話⛩️
こんにちは!cordeliaです。
2024年12月、React TokyoというReactの世界を広げることを目的としたコミュニティが誕生しました。その窓口としてのWebサイトはWaku⛩️というフレームワークで作られています。幸運なことにそのサイト開発に私も参加させていただいたんですね☺️ そこでWakuを初めて触ったので、どのようなものか皆様に紹介します。
Waku⛩️とは
WakuはReactを使ったミニマルなフレームワークです。小規模から中規模なプロジェクトを素早く開発できるように設計されており、Jotai
Zustand
Valtio
といった状態管理ライブラリの作者である@dai_shiさんによって開発されています。
そして先日アップデートされたばかり。めでたい🎉
React Server Componentsに対応
まずは何といってもこれですね。React Server Components(以下RSC)とはコンポーネントの新しいレンダリング方法です。
簡単に説明するとサーバー上で実行される非同期コンポーネントです。HTMLを構築する前段階のコード(例えばデータフェッチやシンタックスハイライトなどのクライアントに不要な処理)はサーバー上でのみ実行され、バンドルされません。その為クライアントへのデータ送信量を減らすことができます。詳しくはドキュメントをご覧ください。
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
を見てみると以下のようになっています。
// 一部省略
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
です。これはルーティングの型定義ファイルで、ページコンポーネントが追加されると自動的に生成されます。
// 一部省略
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
のようにすればセグメントルートにも対応できます。以下はよくあるブログ一覧とブログ詳細ページの例です。
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;
};
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
がデフォルトになっています。グローバルなコンポーネントやデータはこのファイルに書きます。
// 一部省略
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
です。
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;
// 何か処理が続く
};
デプロイ
現在はVercel
とNetlify
がデプロイ先として推奨されています。experimentalですがCloudflare
やAWS 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も覗いてみてください。喜びます🎉
以上、最後まで読んでいただきありがとうございました🙇
Discussion