Hono x Next.js でキャッシュしないRPCクライアントを使いたい
こんにちは!
最近個人的に推している開発環境は Hono x Next.js です。Next.js Route Handlers の処理を Hono で構成します。
構築方法は、Hono 公式ドキュメントか、
Tsuboi 氏の Zenn 記事が参考になります。
遭遇した問題
Server Component 上で Hono RPC を使うと、ビルド時にリクエストしてしまいエラーに遭遇しました。
bunx next build
▲ Next.js 14.2.4
Creating an optimized production build ...
✓ Compiled successfully
.
. (省略)
.
✓ Collecting page data
Generating static pages (0/7) [ ]TypeError: fetch failed
at node:internal/deps/undici/undici:12502:13
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async A (/Users/yoshikouki/src/github.com/yoshikouki/honon/.next/server/app/page.js:14:37391) {
digest: '3496390840',
[cause]: AggregateError [ECONNREFUSED]:
at internalConnectMultiple (node:net:1117:18)
at afterConnectMultiple (node:net:1684:7)
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
code: 'ECONNREFUSED',
[errors]: [ [Error], [Error] ]
}
}
TypeError: fetch failed
at node:internal/deps/undici/undici:12502:13
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async A (/Users/yoshikouki/src/github.com/yoshikouki/honon/.next/server/app/page.js:14:37391) {
digest: '3496390840',
[cause]: AggregateError [ECONNREFUSED]:
at internalConnectMultiple (node:net:1117:18)
at afterConnectMultiple (node:net:1684:7)
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
code: 'ECONNREFUSED',
[errors]: [ [Error], [Error] ]
}
}
TypeError: fetch failed
at node:internal/deps/undici/undici:12502:13
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async A (/Users/yoshikouki/src/github.com/yoshikouki/honon/.next/server/app/page.js:14:37391) {
digest: '3496390840',
[cause]: AggregateError [ECONNREFUSED]:
at internalConnectMultiple (node:net:1117:18)
at afterConnectMultiple (node:net:1684:7)
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
code: 'ECONNREFUSED',
[errors]: [ [Error], [Error] ]
}
}
Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-error
TypeError: fetch failed
at node:internal/deps/undici/undici:12502:13
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async A (/Users/yoshikouki/src/github.com/yoshikouki/honon/.next/server/app/page.js:14:37391)
✓ Generating static pages (7/7)
> Export encountered errors on following paths:
/page: /
このエラーは Next.js 15 RC や、 RC で PPR と ReactCompiler を有効にしていても同様に発生します。
事前にビルドできる分はしておく Next.js の最適化ではありますが、ケースによっては余計です。また、個人的には、事前ビルドやキャッシュは自分でコントロールしておきたいです。特にキャッシュ事故が怖い。
起因
エラーの起因は Hono RPC が発行する fetch を Next.js がキャッシュ対象と判定しているためです。
キャッシュをオプトアウトするには、Next.js 公式ドキュメントで7つの方法が挙げられています。
- The cache: 'no-store' is added to fetch requests.
- The revalidate: 0 option is added to individual fetch requests.
- The fetch request is inside a Router Handler that uses the POST method.
- The fetch request comes after the usage of headers or cookies.
- The const dynamic = 'force-dynamic' route segment option is used.
- The fetchCache route segment option is configured to skip cache by default.
- The fetch request uses Authorization or Cookie headers and there's an uncached request above it in the component tree.
Next.js のキャッシュを無効にする Hono RPC を作る
上記を参考に、キャッシュ無効な Hono RPC とキャッシュ可能なものを用意します。
import { hc } from "hono/client";
import type { AppType } from ".";
const apiUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:8888/api";
export const cacheableClient = hc<AppType>(apiUrl);
export const client = hc<AppType>(apiUrl, {
fetch: (input: RequestInfo | URL, requestInit?: RequestInit) =>
fetch(input, {
cache: "no-cache",
...requestInit,
}),
});
後はこれを ServerComponent で呼び出すだけです
import { client } from "@/server/client";
export const RootPage = async () => {
const res = await client.time.$get();
const { message } = await res.json();
return (
<main className="flex flex-col items-center justify-center gap-10 py-40">
<div className="text-2xl tabular-nums">S: {message}</div>
</>
);
};
おわりに
この記事では、Hono RPC を Server Component で使用する際にキャッシュ無効にする方法について述べました。
ここからは雑感を書き連ねます。
そもそもこの実装では、Next.js から自分自身へ HTTP リクエストしているためオーバーヘッドがあります。また、利用するエンドポイントが Cookie で保護されていた場合などには別の問題が発生するでしょう。
理想は Hono RPC のインターフェースと型だけを利用したいのですが、現状上手く行っていません。「クライアントへAPIを公開しつつ、Server Component からも型・検証付きのインターフェースとして利用できる」みたい状況を夢見ています。
しかし、そもそも RPC がそのような目的を持っているわけではないため、Hono が MVC2 モデルでいう Controller レイヤーとその呼び出しという風に捉えると、別のレイヤーで実現すべきなのかもしれません (例を挙げると、Hono Routing から呼び出す Service レイヤーのようなものを導入し、Server Component からはそちらを叩くなど)
ここら辺は実際にアプリケーションを作って行く中でより良い形を見つけて行こうと思います。
Discussion
見当違いでしたら申し訳ないのですが、もしかしたら以下のプルリクで追加された
init
オプションが使えるかもしれません。