React Router v7 + HonoをClineと共に開発した感想
流れ
もともと自分が作っていたWebアプリを刷新したいなぁとずっと思っていましたがなかなか重い腰が上がらず・・・
昨今の生成AIのおかげで面倒なこともサクッとできる感じがしてきたので、ちょっとやってみようか、となりました。
せっかくなのでモダンなFWとか使いたいなぁと思っていたところで、RemixとReact Routerが統合されたとのニュースを聞いたのをきっかけにReact Router v7でWebアプリを作ってみることにしました。
前提
筆者はVueやNuxtをメインで触っているので、Reactはほぼ初心者です。
SPAやMPAの概念や知識はあるものの、Reactにおけるベストプラクティスなんかはあんまり知らないです(一応Remixについては社内勉強会で知っています)。
技術スタック
React Router v7
今回の肝になります。ReactをベースとしたFWで、デフォルトではSSRになります。そのため、ランタイムが動くサーバが必要になります。今回はSSRで作成しました。
Hono
これも最近Hotなので触ってみようと思って取り上げました。高速に動作するWebフレームワークで、今回はAPIとして使っています。
Cline
VSCodeの拡張機能として使える、AIエージェントです。技術スタックに取り上げるかは微妙ですが・・・
Bun
Node.jsに代わるJSランタイムです。よくパッケージマネージャとしてだけBunを使うという使い方も見られますが、今回はしっかりサーバランタイムとしてBunを使っています。すなわち、最終的にBunをインストールしたコンテナイメージをデプロイしています。
CloudBuild, CloudDeploy
インフラはGCPを採用しました(FirebaseのFirestore本当に便利)。Gitと連携してコンテナイメージのビルド〜レジストリへのアップロードまでをCloudBuildで行い、検証環境や本番環境へのデプロイをCloudDeployで行います。
このあたりはまた別記事にしようと思います。
「AIエージェント」って何だ
今回、初めてAIエージェント(Cline)を使いました。これを使うまでは「CopilotのようなAIコーディング支援ツール」と「AIエージェント」って何が違うのか?をよく分かってなかったです。
厳密な定義は詳しい人に任せるとして、使ってみると以下のような印象でした。
- コーディング支援ツールは、「特定のファイルや特定の行にフォーカス」しています。すなわち、次にどのようなコードを提案すべきかに集中しています。
- その際、追加で必要な情報を要求したり自分で探そうとしたりはしません。あくまでユーザが明示的に与えた情報(とAI自身が持つ知識)に基づいて判断します。
- AIエージェントは、「プロジェクト全体やこれから実行すべき操作や処理全般を考慮」しています。やりたいことを踏まえた上で、複数ファイルの読み取り・書き込み・コマンドの実行など高次元な操作を行います。
- どのような情報があればその目的を達成できるかまで自律的に判断します。
総じて、「やりたいことが広い・大きい、具体的に何をすればいいかからAIに判断してほしい」場合はAIエージェントが向いている気がします。
もちろんAIエージェントは万能ではなく、以下のような点が気になります。
- 無限ループなどに陥る
- とりあえず数百行書いてみて、エラーがあればまるごと修正しようとするので、あちらを直せばこちらが壊れる、みたいなことがたまに起きる
- 「何度もエラーになるので、一旦立ち止まるか」みたいことをしてくれない
- ちょっとした修正を行いたいときはコーディング支援ツールの方が便利
- 世間一般であるあるなコードだったり、似たような処理を繰り返す場合はすぐにそれを察して提案してくれるので手間が少ない
- 金がかかる
- 正直これが一番痛い
- 賢いからありがたいんだけど、内容によってはあっという間に$10とか飛んでいく
とはいえ、今後はAIエージェントを使いこなすスキルが必須になるのは避けられない気がしています。ライブラリやフレームワークのキャッチアップだけでなく、AIツールのキャッチアップまでしないといけない時代ですね・・・
React Router と Hono の接続
全体としてはReact Routerアプリとして作りつつ、API部分をHonoにするにはアダプタの設定が必要になります。
幸い、ちょうどそのユースケースに合うライブラリが作られているので、今回はありがたくそれを使いました。
基本的な使い方はREADMEに書いているので、簡単に抜粋します。
vite.config.ts
に追記する
注意点は{ runtime: "bun" }
を追加することです。これを書かないとBunで動いてくれません。
import { reactRouter } from "@react-router/dev/vite";
import { reactRouterHonoServer } from "react-router-hono-server/dev"; // add this
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
reactRouterHonoServer({ runtime: "bun"} ), // add this
reactRouter(),
tsconfigPaths()
],
});
app/server/index.ts
を作成する
app/server/index.ts
にHonoサーバの定義を記述します。app/server.ts
でも大丈夫です。
下記ファイルのapi
にあたるものは、別ファイルで定義しています(この後作成します)。
ちなみに、ここでcreateHonoServer
を**react-router-hono-server/bun
からimportするのを忘れないでください**。筆者はreact-router-hono-server/node
からimportしてしまって時間を溶かしました。
import { Hono } from "hono";
import { createHonoServer } from "react-router-hono-server/bun";
import { API_BASENAME, api } from "./api";
import { getLoadContext } from "./context";
// Create a root Hono app
const app = new Hono();
// Mount the API app at /api
app.route(API_BASENAME, api);
export default await createHonoServer({
// Pass the root Hono app to the server.
// It will be used to mount the React Router app on the `basename` defined in react-router.config.ts
app,
getLoadContext,
});
app/server/api.ts
を作成する
具体的なハンドラはこちらに定義します。全部app/server/index.ts
でもいいっちゃいいと思いますが、ハンドラの定義とそれ以外を分けた方が可読性は高そうです。
READMEでは、/api
というプレフィックスをreact-router.config.ts
で設定するようにしていますが、筆者の環境だとうまく動かなかったのでapp/server/api.ts
側でHonoの機能として/api
にマウントしています。いろいろとトラブルシューティングした結果この方法に行き着いたので、本当は筆者の設定が間違っていたせいも考えられますが、動いているのでヨシ!としています。
後述のRPCを使うために、route
を宣言した上でtypeof
で型を生成しています。
const API_BASENAME = "/api";
const api = new Hono().basePath(API_BASENAME);
const route = api.get(...);
export type AppType = typeof route;
export { api, API_BASENAME };
React Routerの書き味
Remix時代からの思想ですが、「サーバこそ信頼できる唯一の情報源(SSOT)である」というのが受け継がれている印象を受けました。各ルート(React Routerにおけるページを指す言葉)には、loader
とaction
という関数を定義できます。
loader
そのルートにアクセスしてSSRが始まる前にサーバサイドで実行される関数です。その名前の通り、主に外部のデータソースからデータを持ってくるために用いられます。この関数の戻り値は、ルートコンポーネントの引数に含まれます(=コンポーネント側で参照できる)。
action
そのルートからのPOSTリクエスト(他のメソッドでもよい)で発火してサーバサイドで実行される関数です。受け取ったデータを使って処理を実行できます。例えばそのデータを加工して外部のDBに永続化する、なんてことができます。
処理の結果に応じて別のルートに遷移させることもできます。
所感
ルートをレンダリングする上で必要なデータを持ってきたり、画面上で入力したデータを永続化したりするロジックをサーバサイドに任せることができます。もちろん、セキュリティなどからそうするのが一般的ではあるのですが、これらのロジックたちを一つのルートコンポーネントに紐づけて記述できるので、コードの見通しが非常によかったです。
面白いのがloader
→レンダリング→action
のシームレスなループで、form
をPOSTするとaction
が実行されるのですが、明示的に他のルートに遷移させない限りは、同じルートをloader
を通じて再レンダリングしてくれます。すなわち、コード上でわざわざ再フェッチさせる処理を書かなくてよいのがありがたいです。
SSOTの話に関連しますが、個人的にReact Routerではクライアントサイドでの状態管理を頑張ることは考えていない印象を受けました。もちろん動いているのはReactなのでuseEffect
といったhooksを使うことはできますが、どちらかというとクエリパラメータやCookieといった、Web標準かつJSに頼らない方式を重んじているようです。
Honoの書き味
RPCが便利ですね。エンドポイントもリクエストやレスポンスの型も型補完が効くので、スイスイコーディングできちゃいます。
const client = hc<AppType>(process.env.API_BASE_URL || baseUrl);
バリデーションライブラリとミドルウェアのシナジーも強く、リクエストのバリデーションも簡単にできました。このあたりはピュアなHonoの機能というよりはコミュニティやエコシステムの強さと言ったところでしょうか。
下記は、Typiaというライブラリを使ってPUTリクエストのバリデーションを行うハンドラの定義です。ロジック自体は外に定義し、ハンドラはpassしたときのロジックに集中できます。
.put("/events/:id", typiaValidator("json", addEventValidate), async (c) => {
await eventInfra.update(c.req.param("id"), await c.req.json());
return c.json({ success: true });
})
元々はルーティングの高速さをウリにしたFWだと認識しているのですが、今回の規模ではその優位性がはっきりわかるユースケースにはなりませんでした。。。が、今後も採用していこうと思います。
Clineどうだった?
AIエージェントを使ったコーディングを初めてやってみましたが、凄まじい効果でした。書いていることはReactやTypeScriptなので、自分に全くできないことをやってもらっているわけではないですが、いい感じのデザインのものを作ってもらったり、ややこしい配列の計算ロジックなどをパッと書いてくれて、デバッグまでしてくれるのはありがたいですね。
なんか自分の技術力が不要になったみたいで素直に喜べないですが、AIを嫌っていては置いていかれそうなので、AIと仲良くしていこうと思います。
まとめ
Clineを使うことで、面倒なところには時間をかけずに、React Routerのエッセンスに触れることができて、開発体験は非常に良かったです。Reactベースの技術に慣れている人であればぜひ触ってほしいFWです。
Discussion