対象読者
- JavaScriptとWeb開発の基礎に理解がある方
- Reactを用いたJavaScriptアプリケーション開発の経験者
前提環境
筆者の検証環境は以下の通りです。
- macOS Ventura 13.4
- Node.js 20.2.0/npm 9.6.6
- React 18.2.0
- Next.js 13.4.4
より直感的なルーティング、よりサーバーを活用したレンダリング
Next.jsは、ReactでWebサイトやWebアプリケーションを作成するための、主にルーティング(画面遷移)に責任を持つフレームワークとして、デファクトスタンダードと呼ぶに相応しい立ち位置で使われるようになりました。2016年に登場し、ブラウザで動くだけなのが当たり前だったReactをサーバー側でレンダリングする(サーバーサイドレンダリング)というアプローチの先駆者として、この分野を牽引しています。ただ、時代の要請に応えたり、後発のフレームワークにインスパイアされた新機能を取り込んでいくうちに、ルーティング機構が複雑になるのが近年の課題になっていました。
そこで、Next.jsチームは、Reactの次の時代を牽引すべく、フレームワークの根幹ともいえるルーティング機構を再設計しました。それがNext.js 13.4で安定版がリリースされたApp Router(アップルーター) です(図1)。
App Routerには大きな特徴が2つあります。ファイルパスの使い方の刷新と、レンダリングにおけるサーバーの活用です。
ファイルパスの使い方を刷新
まず一つ目として、ファイルパスの使い方の刷新です。フォルダ内のファイルパスでルーティングを決定する方式(file-system based router)であることは従来と変わりませんが、フォルダ名やファイル名の命名規則が大きく変わりました。
その一環として、App Routerではトップレベルのフォルダとして pages/
ではなく app/
を利用します。App Routerという名前は、このフォルダ名に由来しており、従来のルーティング機構はPages Routerと呼ばれることになりました。
Pages Routerでは、レイアウトと呼ばれる機構を後付けした際にAPIが複雑になってしまいましたが、App Routerではレイアウトを初めから設計に組み込んでいるため、直感的に扱えるようになっています。
サーバーをより活用したレンダリング
二つ目としては、レンダリングにおけるサーバーの活用が挙げられます。ここでいうレンダリングとは、あるデータに基づいて、条件分岐や反復処理を行い、コンポーネントの構造を決定する処理を指します(必ずしもHTMLの生成は伴いません)。
Pages Routerにも、画面遷移時にサーバー側でデータ取得を行う機能はありました。しかし、レンダリングまでサーバー側で請け負うのは、ページを直接リクエストする初期表示のときのみで、画面遷移後の(複雑な条件分岐や反復処理を含む)レンダリングや、ページ内の個別のコンポーネントによる非同期通信は、ブラウザ側に任されていました。
一般論として、ブラウザの処理性能や通信の速度は、ユーザーの使っているネットワークやデバイスの性能に強く依存します。サーバーの性能はサービス提供者(この記事を読んでいるあなたも、おそらくそうでしょう)がチューニングしたりお金を払うことで向上させることができますが、ユーザーが個別の事情で型落ちのスマホや格安通信事業者を利用することに対しては、何の対処もできません。Pages Routerのアプローチでは、サーバー側で処理できる比率が頭打ちしやすかったのです。
そういった課題もあってか、App Routerでは思い切った戦略が取られました。原則として、 全てのReactコンポーネントをサーバー側でレンダリングすることにした のです。通信も、条件分岐も、反復処理も、全てサーバーで行えば、ユーザーの環境によってパフォーマンスが落ちる要因を最低限にできます。
App Routerを支える技術「React Server Components」
App Routerのレンダリング戦略を後押しするように、Reactというライブラリそのものにも、サーバーでレンダリングすることに特化した記法が追加されました。それが React Server Components(RSC)と呼ばれるコンポーネントです。厳密にはNext.jsではなくReact側の新機能ですが、初めての活用事例がNext.jsなので、Next.jsのドキュメントで詳細に解説されています。
具体的には、リスト1のようなコンポーネントをサーバー側で事前に処理できるようになりました。
// (1)
export async function ArticleComponent({ id }) {
// (2)
const res = await fetch(https://api.example.com/articles/${id}
);
if (!res.ok) {
throw new Error('データの取得に失敗しました');
}
const data = await res.json();
return (
<article>
<h2>{data.title}</h2>
<div>
{data.content}
</div>
{/* コメントがある場合だけコメント欄を表示する */}
{data.comments.length > 0 ? (
<div>
<h3>コメント</h3>
<ul>
{data.comments.map((comment) => (
<li key={comment.id}>{comment.author} {comment.text}</li>
))}
</ul>
</div>
) : null}
</article>
);
}
Reactの経験が長い方にとっては、(1)のように関数コンポーネントをasync functionとして定義していることも、(2)のように関数コンポーネントのスコープ内で通信をしていることも、極めて奇妙なことに感じられると思います。
当然ながら、このコンポーネントをブラウザで実行することはできません。サーバーで処理されて、通信や条件分岐や反復処理を終えた、最終的なビューツリーのデータだけがブラウザに送られます。このデータはHTMLではなくJSONLライクな独自形式になっており、軽量かつ柔軟にツリーを分割しながらブラウザに送信できるのですが、本記事の本筋ではないので、割愛させてください。
さて、リスト1では最も特徴的な、async functionの直下で通信を行うコンポーネントを例に挙げましたが、Next.jsのApp Routerでは、リスト2のような素朴なコンポーネントも、Server Componentsとして処理されます。
export function Text({ children }) { return <span>{children}</span>; }
ブラウザ側でレンダリングや状態管理が行われるコンポーネントは、明示的に「これはブラウザ側でレンダリングしてほしい」というおまじない(次回以降に解説)を記載したものだけになります。
繰り返しになりますが、App Routerを利用すると、原則として全てのReactコンポーネントがServer Componentsとして処理されます。これは、従来のPages Routerや、シングルページアプリケーションでのReactと比較しての、最も大きな違いとなりますので、React経験者がApp Routerを学ぶ場合には、よく意識していただければと思います。