メタフレームワークのサーバコンテキストの扱い方と設計思想
きっかけ
Xを見ていたら以下のような議論を見かけました(確か別のディスカッションだった気がするけど、以下も類似の問題)。
現在のNext.js(App Router)では、layout.tsx
とpage.tsx
、それ以外のコンポーネントで、取得できるサーバコンテキスト(Requestオブジェクトなどのアクセス情報)が異なります。CookieやHeaderは関数を通じて全てのサーバコンポーネントで利用できますが、パス周りをサーバコンポーネントで扱おうとすると結構不便を強いられます。上記のPRはこれを解決するためのものです。
import { cookies } from 'next/headers'
export default function Page() {
const cookieStore = cookies()
const theme = cookieStore.get('theme')
return '...'
}
import { headers } from 'next/headers'
export default function Page() {
const headersList = headers()
const referer = headersList.get('referer')
return <div>Referer: {referer}</div>
}
page.tsx
ではパス周りのコンテキストにアクセス可能であり、Propsとしてパスパラメータ(params
)やクエリパラメータ(searchParams
)にアクセスできます。
export default function Page({
params,
searchParams,
}: {
params: { slug: string }
searchParams: { [key: string]: string | string[] | undefined }
}) {
return <h1>My Page</h1>
}
しかし、layout.tsxではPropsとしてアクセスできるのはchildren
のみであり、page.tsxでアクセスできたパス周りのデータを受け取ることはできません。ページ遷移しても上位のレイアウトは使い回されるため、サーバサイドのパスに依存する処理が初回の一度しか実行されないためでしょう。クライアントコンポーネントであれば、パスの変更をイベント経由で監視可能であり、上記のような問題は発生しません。遷移時に必ず再評価されるtemplate.tsx
もありますが、こちらもサーバコンポーネントだとパス周りを触れません。
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return <section>{children}</section>
}
export default function Template({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}
layout.tsx
と同様にサーバサイドコンポーネント自体もパス周りのコンテキストに触る事はできません。クライアントコンポーネントで実装するか、Props渡しを頑張る他ありません。
'use client'
import { useParams } from 'next/navigation'
export default function ExampleClientComponent() {
const params = useParams<{ tag: string; item: string }>()
// Route -> /shop/[tag]/[item]
// URL -> /shop/shoes/nike-air-max-97
// `params` -> { tag: 'shoes', item: 'nike-air-max-97' }
console.log(params)
return <></>
}
'use client'
import { useSearchParams } from 'next/navigation'
export default function SearchBar() {
const searchParams = useSearchParams()
const search = searchParams.get('search')
// URL -> `/dashboard?search=my-project`
// `search` -> 'my-project'
return <>Search: {search}</>
}
Next.jsとしては、クライアント(ブラウザ)の状態に依存するパス周りは原則クライアントコンポーネントとして扱えという方針なんだと思います。useParams
やuseSearchParams
があるため、おそらくハイドレーションによる事前生成は可能だと思いますが、できれば純粋なサーバコンポーネントとして扱えるようになると嬉しいですね。
ここから本題
では他のメジャーなフレームワークではどうなっているのでしょうか?筆者の偏見と経験から以下を比較することにします。
- Astro
- Remix
- Qwik City
Astro
基本的には非常にシンプルです。Astroコンポーネントであれば、どの階層にいてもAstro
グローバルオブジェクト経由で生のRequest
やパスパラメータ、クエリパラメータを取得できます。これはNext.jsなどのように特殊なルーティング(に見せかけた履歴操作)ではなく、古典的なMPAとして都度HTMLを生成する方針のため、クライアントサイドのパスの切り替えをケアする必要がないためでしょう。
Astroコンポーネントであれば、サーバサイドの処理もコンテキスト絡めて自由に記述できるため、コンポーネントというよりも小さな完結したHTMLを組み合わせる方が近いと思います。
<h1>The current URL is: {Astro.url}</h1>
<h1>The current URL pathname is: {Astro.url.pathname}</h1>
<h1>The current URL origin is: {Astro.url.origin}</h1>
一方で、Next.jsのようなクライアントサイドも巻き込んだサポートは基本ありません(後述の場合を除く)。クライアントサイドのUIライブラリは、Reactを始め様々なものが選択可能であり、それぞれをサポートする必要があることや、ブラウザの標準APIが存在するため、余計なレイヤーは取り除いていんだと思います(Qwikのサンプルではイベントによる状態の共有が提案されています)。
なお、クライアントディレクティブ次第で初回レンダリングの環境が変わるため、useEffect()
のようなクライアントサイドで動作することが確実な処理を使った方が無難ですが、client:only
であれば絶対にクライアントサイドで実行されるため、useEffect
の外でブラウザのAPIを直接叩くことも可能です。
RemixとQwik City
このあたりはフレームワークの思想的にも利用側の実装的にも割と似ていそうな感じです。Next.jsと異なり非同期コンポーネントは存在しません。また、routes/
配下(たまたま同じ)のページコンポーネントになにか特別な値が渡されることはありません。そのため、基本的にはNext.jsにおけるクライアントコンポーネントベースであり、Hook経由でパスやサーバサイドの値を取得する事ができます。
内部の実装は見ていませんが、おそらくサーバサイドではRequest、クライアントサイドではlocation
オブジェクトをラップしてるんだと思います(Next.jsも同じ?)。
// Remix
import { useLocation } from "@remix-run/react";
function SomeComponent() {
const location = useLocation();
// ...
}
// Qwik
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export default component$(() => {
const loc = useLocation();
return <div>Hello {loc.params.username}!</div>;
});
サーバサイドの処理はLoader(BFF + Fetch + Tanstack Queryみたいなものを自動生成する機能)を経由して取得することが可能であり、そちらであればAstroと同じ用にRequestのようなコンテキストにフルアクセス可能です。Loaderを独立したHookとして定義するQwikと、基本的にはページに紐づかせるRemixで違いはありますが、使用感は似たものになると思います。
これはNext.jsのPages RouterのgetServerSideProps
と同じようなものではないか?という意見もあると思いますが、前述したようにTanstackのようなMutation後の自動更新も含まれています(App RouterのrevalidatePathが自動で実行される感じ)。加えて、型定義なども非常に綺麗に統合されているため、扱いやすさはかなり改善されていると感じます。
まとめ
ここ1、2年ほどはサーバサイドに力を入れているメタフレームワークですが、調べてみると結構仕様が異なります。現時点では仕様の理解しやすさ、コードの書き易さ、パフォーマンスの観点で以下のように感じています。
扱いやすさ:Remix > Qwik > App Router > Pages Router
最適化:App Router => Qwik > Remix > Pages Router
以下のスライドで、App RouterはF1である、というのは言いえて妙だなと。カリカリにチューニングできれば最高のパフォーマンスを発揮できそうですが、個人的にはそこまでする必要のアプリケーションってほとんど無いと感じています。(落とし穴が多い印象、技術力不足を感じるところではありますが…)
慣れたフレームワークで!ツールチェーン重視で!というのも否定はしませんが、近年ではViteベースが主流になりつつある気がしますし、必ずしもApp Routerを使う必要はない気がします(Reactを使いたいだけならRemixでもいい)。このあたり、様々な要因を含めて技術選択していきたいですね。
Discussion