【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.tsx
Server 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
のアップデートが早く情報についていけてないという方にも理解が及べば幸いです。ありがとうございました。