【Next】App Routerの紹介
こんにちは、株式会社Gizumoでエンジニアをしている田中です。
Next.js 13.4の発表でApp Routerが正式にリリースされ、安定版(Stable)となったためプロダクションコードで懸念なく利用可能になりました。
今回はApp Routerの中でとくに重要なServer ComponentsとNested Layoutsを解説しつつ、Next.jsの中でもメイントピックなレンダリングについて検証と解説を行っていきます。
目次
前提・対象とする読者
この記事は以下を前提として読み進めてください。
- サンプルコードは
TypeScriptで記述 React 18で追加された機能やSSR等については細かく解説しないNext.js 13.4で同時発表された、TurbopackとServer Actionsについては解説しない
また、対象とする読者は以下のような方を想定しています。
Next.jsを知っている・使ったことがある方React 18についてなんとなく理解がある方SSRやSSG等のレンダリング手法について理解がある方
App Routerとは
App RouterはNext.jsのルーティングのベースとなる機能です。/appディレクトリ配下に切ったディレクトリやファイルはルーティング対象となり、ページ遷移が可能です。
これにより今まで/pages配下で行っていたことを、これからは/app配下で行うようになります。
以下の構成であれば、「/」にアクセスするとapp/page.tsxが表示され、「/articles」にアクセスするとapp/articles/page.tsxが表示されるようになります。
app
├── articles
│ ├── [articleId]
│ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
├── layout.tsx
└── page.tsxServer Components
Next.jsのApp RouterはReactのServer Componentsをデフォルトコンポーネントとして使用します。
Server Componentsを簡単に説明すると、SSRを行いながらページの静的な部分は先に返しつつ画面上でインタラクティブにしながら、データフェッチングを部分的に待つことができる技術です。
Client Componentsの使用
Server Componentsがデフォルトコンポーネント化するため、Client Componentsを使用する時には'use client'宣言が必要になります。
Client ComponentsとはuseState等クライアントの状態を扱うAPIを使用するコンポーネントやonClickやonChange等DOMイベントを内包するコンポーネントのことです。
参考:Client Components
'use client'
export const Hoge = () => { ...省略 }シンプルになったデータフェッチング
データフェッチは今までgetStaticPropsやgetServerSidePropsを用いてレンダリングに合わせたデータフェッチメソッドを使用する必要がありました。
App Routerでは以下のようにコンポーネントレベルでasync/awaitが使用可能になりシンプルなデータフェッチングが可能になりました。また、fetchメソッドのcacheオプション等の指定方法によってデータフェッチ方法が変化します。データフェッチ方法についてはレンダリングの検証で詳しく解説します。
// components/ArticleList.tsx
const fetchArticles = async () => {
const response = await fetch('url', {
cache: "no-store",
})
return response.json()
}
export const ArticleList = async () => {
const articles = await fetchArticles()
return (...省略)
}Streaming HTML
上記のcomponents/ArticleList.tsxコンポーネントはServer Componentsとして配信され、レンダリングが完了するまでSuspenseによってフォールバックコンテンツが表示されます。レンダリングが完了すると、Suspenseが解決され、フェッチデータの埋め込まれたHTMLが表示されます。
通常のSSRでは、下の例で言うArticleListのレンダリングが完了するまでHTMLを配信できませんでしたが、Server ComponentsとSuspenseによってデータ取得を待たずにHTMLを配信し先にインタラクティブな要素を提供できるようになりました。
このような挙動はStreamingやStreaming HTMLと呼ばれます。
※フォールバックコンテンツはローディングUI等を指します。
// app/articles/page.tsx
import { ArticleList } from '../../components/ArticleList'
export const Page = () => {
return (
<div>
<Header />
<SideBar />
<Suspense fallback={<div>Loading...</div>}>
<ArticleList />
</Suspense>
</div>
)
}以下の挙動がSSR時の初期表示にて実現可能
Server Componentsを利用すると、以下の挙動がSSR時の初期表示にて実現可能です。SSRを行いながら段階的なコンテンツの取得が可能になっています。
以下の例の緑の部分は画面に表示済み、黄色の部分はデータ取得中でフォールバックコンテンツが表示されています。
データ取得中…

データ取得後

Nested Layouts
Next.jsのApp Routerではlayout.tsxファイルを/app配下に配置することにより、/app配下のすべてのページで共通のレイアウトを適用できます。Nested Layoutsと呼ぶように、子ディレクトリが持つlayout.tsxは親ディレクトリのlayout.tsxを継承します。
以下の構成で言うと、/articles配下はapp/layout.tsx継承、/articles/[articleId]配下はapp/articles/layout.tsxを継承します。
app
├── articles
│ ├── [articleId]
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
├── layout.tsx
└── page.tsx以下はページ遷移によるlayout継承の例

layoutsファイル群
Next.js 13.4には、ファイルの規約に関するいくつかの変更点があります。これらの変更は、Next.js開発体験を向上させます。
以下ではFile Conventionsの中から主要なファイル群を紹介します。
loading.tsx
loading.tsxファイルは、ページの初期表示時レンダリングされるコンポーネントです。このファイルは、/appディレクトリ直下もしくは/appディレクトリのサブディレクトリに配置可能です。配置したディレクトリ配下のページ単位のフォールバックコンテンツとして利用されます。デフォルトはServer Componentsで、Client Componentsとしても利用できます。
たとえば、次のようなコードを使用します。該当のページコンテンツが表示されるまで「Loading…」が表示されます。
export default function Loading() {
return 'Loading...';
}error.tsx
error.tsxファイルは、ページの取得時エラーが発生した際にレンダリングされるコンポーネントです。このファイルは、/appディレクトリ直下もしくは/appディレクトリのサブディレクトリに配置可能です。配置したディレクトリ配下のページ単位のフォールバックコンテンツとして利用されます。必須でClient Componentsとして利用する必要があります。
たとえば、次のようなコードを使用します。
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>
Try again
</button>
</div>
);
}not-found.tsx
not-found.tsxファイルは、存在しないルートにアクセスもしくは'next/navigation'のnotFound()が呼ばれた時にレンダリングされるコンポーネントです。このファイルは、/appディレクトリ直下か/appディレクトリのサブディレクトリに配置可能です。
たとえば、次のようなコードを使用します。
export default function NotFound() {
return (
<div>
<h1>404 Not Found!</h1>
</div>
);
};レンダリングの検証
最後にServer Componentsで少し紹介したfetch関数のcacheオプション等の指定によりレンダリング方法がどのように変化するのか、実際にビルドして検証したいと思います。
公式のData Fetchingを参考に、force-cache、no-store、revalidateを用いてデータフェッチングを行ないます。それぞれに対応するページを用意し、記事の詳細ページのように一意なIDによって表示内容が異なるページも用意します。詳細ページはgenerateStaticParamsを使用してビルド時にIDを取得し、それをもとに詳細ページを静的生成します。
最終的なページ構成は以下になります。
app
├── articles
│ ├── layout.tsx
│ ├── page.tsx
│ ├── [articleId]
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── force-cache
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── no-store
│ │ ├── layout.tsx
│ │ └── page.tsx
│ └── revalidate
│ ├── layout.tsx
│ └── page.tsx
├── layout.tsx
└── page.tsxまた、最終的な結果は以下になります。
force-cache: Staticno-store: SSRrevalidate: Staticforce-cache with generateStaticParams: SSG

force-cache : Static Data Fetching
force-cacheは、cacheオプションのデフォルト値です。
force-cacheは、ページの静的生成時に有効です。ビルド時に埋め込まれたデータが毎度表示されるため、リクエストの度にデータフェッチングが走らず高速な初期表示を実現します。
以下はデータフェッチングの例です。
const articles = await fetch('https://jsonplaceholder.typicode.com/posts',{
cache: 'force-cache',
}).then(res => res.json())force-cache with generateStaticParams
force-cacheは、generateStaticParamsと組み合わせることで、詳細ページ等の動的なパスを持つページに対しても静的生成を行うことができます。
以下はデータフェッチングの例です。
import { Suspense } from "react";
const ArticleListForceCache = async ({ articleId }: { articleId: string }) => {
const articles = await fetch(`https://jsonplaceholder.typicode.com/posts?id=${articleId}`,{
cache: 'force-cache',
}).then(res => res.json())
return (
<div>{articles[0].title}</div>
)
}
type Props = {
params: {
articleId: string
}
}
export default function page({ params }: Props){
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
{/* @ts-ignore */}
<ArticleListForceCache articleId={params.articleId} />
</Suspense>
<span className="text-xl">Article Detail</span>
</div>
)
}
type Article = {
userId: number,
id: number,
title: string,
body: string
}
export async function generateStaticParams() {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_limit=10`)
const articles: Article[] = await res.json()
return articles.map((article) => ({
articleId: article.id.toString(),
}))
}
no-store : Dynamic Data Fetching
no-storeは、ページのSSR時に有効です。ページ取得時にサーバーサイドでデータフェッチングが走ります。こちらはServer Componentsによってレンダリングを部分的に待てるようになりました。
以下はデータフェッチングの例です。
const articles = await fetch('https://jsonplaceholder.typicode.com/posts',{
cache: 'no-store'
}).then(res => res.json())revalidate : Revalidating Data
revalidateオプションはデータの再検証のためのインターバルタイムを指定できます。たとえばrevalidate: 60を指定すると60秒ごとにデータの再検証が行われます。このオプションは、cacheオプションがforce-cacheの場合に有効です。データの再検証はキャッシュサーバー等で行われます。これはISR(Incremental Static Regeneration)と呼ばれるレンダリング手法になります。
以下はデータフェッチングの例です。
const articles = await fetch('https://jsonplaceholder.typicode.com/posts',{
next: { revalidate: 60 }
}).then(res => res.json())最後に
いかがだったでしょうか。Next.jsのアップデートが早く情報についていけてないという方にも理解が及べば幸いです。ありがとうございました。