はじめに
この記事ではNext.jsにvesrion 9.3から導入された以下の3つのAPIについて解説します。
getStaticPropsgetStaticPathsgetServerSideProps
こちらのissueで議論されてきた機能です。
Static Generation / SSG Improvements · Issue #9524 · zeit/next.js
GitHub追っているとGS(S)P methodsと略されているのを見ます。
Next.jsが未経験の方でもReactとSSR(Server Side Rendering)の基本的な知識があれば理解できると思います。
Next.jsを「SSRを簡単に実現できるフレームワーク」と捉えてる方は是非読んでみてください。
注意事項
現在の最新安定版v9.2.2では使用できません。v9.2.3-canary.16から使用できます。
Release v9.2.3-canary.16 · zeit/next.js
そのためvesrion 9.3.0のリリース時には仕様変更が伴う可能性があります。
とは言えレポジトリを追う限りここから大きく仕様が変更されることはないと思います。何か変更があれば追記する予定ですので心配な方はリリース後に読むことをオススメします。
(追記)
Blog - Next.js 9.3 | Next.js
リリースされました🎉🎉
この記事に大きな変更はありません。公式のリリースブログやドキュメントも充実しているので必ず目を通しましょう。
(さらに追記)
Polyfill fetch by default by timneutkens · Pull Request #12353 · zeit/next.js
Previously users would need to import isomorphic-fetch or isomorphic-unfetch, this won't be needed once this has landed.
このPRでisomorphic-unfetchやnode-fetchをインストールする必要がなくなります。
本文は特に変更する予定はありませんが適宜対応してください。
tl;dr
-
getStaticPropsとgetStaticPathsはSSG用のAPI -
getServerSidePropsはSSR用のAPI getInitialPropsは非推奨になる
No deprecations are introduced and getInitialProps will continue to function as it currently does. We do encourage adopting these new methods on new pages and projects.
(追記:リリースブログによると非推奨になるわけではないようです。新規にgetInitialPropsを使用するケースはあまりないと思います。)
(前提知識) SSG と getInitialProps と next export
前提知識となるSSG、getInitialProps、next export についておさらいです。既にNext.jsに慣れている方は適宜読み飛ばしてください。また9.3のリリース以降にNext.jsを使い始めた方も真剣に読む必要はありません。
SSG
Next.jsではpagesディレクトリ内のファイル名に基づいてルーティングされます。
Pages - Documentation | Next.js
function Welcome() {
return <div>ようこそ Next.js へ!</div>
}
export default Welcome
上記のファイルを生成し/welcomeにアクセスすると以下のように表示されます。
<div id="__next">
<div>ようこそ Next.js へ!</div>
</div>
この時pages/welcome.jsの出力は常に同じです。/welcomeにアクセスすると上記のHTMLが必ず表示されます。そのためアクセスの度にレンダリング(SSR)する必要がありません。
Next.jsはこのようなケースでは自動的に静的なファイルをビルド時に生成(SSG)します。
これはAutomatic Static Optimizationと呼ばれます。
next.js/automatic-static-optimization.md at v9.2.2 · zeit/next.js
Next.js automatically determines that a page is static (can be prerendered) if it has no blocking data requirements. This determination is made by the absence of getInitialProps in the page.
つまりpagesフォルダ内のファイルにgetInitialPropsの存在をチェックし、存在しなければ事前に生成が可能と判断します。このgetInitialPropsとはどんなメソッドなのでしょうか。
getInitialProps
next.js/data-fetching.md at v9.2.2 · zeit/next.js
getInitialPropsはページがレンダリングされる前に実行されるAPIです。該当のパスにリクエストがあると実行され、ページに必要なデータをpropsとして渡します。
(実際にはログを送信するなどのHTMLに影響しない副作用を行うこともある。)
今回紹介する3つのAPIと同様にpagesフォルダ内のファイルのみで使用できます。
getInitialPropsがSSR専用のAPIというのは誤解です。
通常のアクセスの場合、getInitialPropsがサーバー側で実行されます。
一方、next/linkを使用してクライアントサイドルーティングした場合にはクライアント側で実行されます。
そのためisomorphic-unfetchといったクライアント側でもサーバー側でも動作するfetchライブラリが推奨されます。
// クライアントサイドでもサーバーサイドでも使用するため
import fetch from 'isomorphic-unfetch'
// getInitialPropsで取得したスター数を受け取る
function Stars({ stars }) {
return <div>Next stars: {stars}</div>
}
// 先に実行される
Stars.getInitialProps = async () => {
const res = await fetch('https://api.github.com/repos/zeit/next.js')
const json = await res.json()
return { stars: json.stargazers_count }
}
export default Stars
ここではNext.jsのGitHubスター数を取得しています。returnされた値はpropsとして受け取り、divに反映させています。
生成されるHTMLは以下の通りです。
<div id="__next">
<div>Next stars: 45601</div>
</div>
45601という値は執筆時点のものです。
リクエストの度にレンダリングが行われるためアクセスするたびに変化する可能性があります。
SSRは便利ですが欠点もあります。
- サーバーを管理する必要がある
- リクエストの度にHTMLを生成する
- 外部のAPIがダウンしているとgetInitialPropsが評価されないためHTMLが生成できない
こうした理由から事前に静的なファイルをビルドしておくことが求められてきました。
next export
外部に依存するデータをビルド時に取得しSSGするためには、これまでnext exportというコマンドが利用できました。next buildと異なりgetInitialPropsが存在するページでもビルド時に評価をし、静的なファイルを生成することができます。
しかしこれには問題がありました。
next linkを使用してgetInitialPropsを使用しているページにクライアントサイドでルーティングを行うと事前にnext exportで生成したファイルではなく再度クライアント側でgetInitialPropsが実行されてしまうのです。
これを解決するために新たなAPIが導入されました。
getStaticProps
やっと本題です。
getStaticPropsはgetInitialPropsが行っていた処理をビルド時に行い、静的なファイルを事前に生成するためのAPIです。next exportと異なりnext linkでルーティングした場合でも静的なファイルを利用します。
このメソッドはクライアント側で実行されることはありません。必ず事前にサーバーサイドで実行されます。
// クライアントサイドでは実行されないため'isomorphic-unfetch'は必要ない
import fetch from 'node-fetch'
// getStaticPropsで取得したスター数とビルド時の時刻を受け取る
function BuildTimeStars({ stars, build_time }) {
return <div>ビルド時({build_time})のNextのstar数は: {stars}</div>
}
// ビルド時に実行される
export async function getStaticProps() {
const res = await fetch('https://api.github.com/repos/zeit/next.js')
const json = await res.json()
const stars = json.stargazers_count
// ビルド時刻の取得
const build_time = new Date().toString();
return {
props: {
stars,
build_time
},
}
}
export default BuildTimeStars
/starsと同様にNext.jsのGitHubスター数を表示するページです。ただし今回はgetStaticPropsを使用してビルド時の時刻もpropsとして渡しています。
このページは外部のAPIに表示結果が依存していますがアクセスの度にSSRしません。ビルド時にデータを取得し静的なファイルを事前に生成します。
表示されるHTMLは以下の通りです。
<div id="__next">
<div>ビルド時(Sat Mar 07 2020 18:44:20 GMT+0000 (Coordinated Universal Time))のNextのstar数は: 45601</div>
</div>
再ビルドしない限りいつアクセスしても同じ結果です。時刻やスター数はあくまでビルド時のものです。
getStaticPropsの次はgetStaticPathsですがその前に Dynamic Routes についておさらいです。
(前提知識) Dynamic Routes
Dynamic Routes - Documentation | Next.js
pagesフォルダ内でファイル名にブラケット[]をつけるとダイナミックルーティングが有効になります。
import { useRouter } from 'next/router'
const Post = () => {
const router = useRouter()
const { pid } = router.query
return <p>Post: {pid}</p>
}
export default Post
<div id="__next">
<p>Post: abc</p>
</div>
<div id="__next">
<p>Post: hoge</p>
</div>
上記のようにアクセスしたパスに応じて表示する内容が変更されます。
getStaticPaths
getStaticPathsはDynamic Routes利用時にも静的なファイルを生成するためのAPIです。
下のファイルはこれまでと同様にGitHubスター数を表示するページです。
zeitが管理するレポジトリを30件取得しDynamic Routesを活用して静的なファイルを生成します。
import fetch from 'node-fetch'
function Zeit({ name, stars }) {
return <div>{name} stars: {stars}</div>
}
// 最初に実行される。事前ビルドするパスを配列でreturnする。
export async function getStaticPaths() {
// zeitが管理するレポジトリを(APIのデフォルトである)30件取得する
const res = await fetch('https://api.github.com/orgs/zeit/repos')
const repos = await res.json()
// レポジトリの名前をパスとする
const paths = repos.map(repo => `/zeit/${repo.name}`)
// 事前ビルドしたいパスをpathsとして渡す fallbackについては後述
return { paths, fallback: false }
}
// ルーティングの情報が入ったparamsを受け取る
export async function getStaticProps({ params }) {
// ファイル名のzeit/[name].jsに対応
const name = params.name
const res = await fetch(`https://api.github.com/repos/zeit/${name}`)
const json = await res.json()
const stars = json.stargazers_count
return { props: { name, stars } }
}
export default Zeit
これをビルドするとターミナルで実際にビルド対象になったパスが表示されます。
● /zeit/[name]
├ /zeit/ms
├ /zeit/micro
├ /zeit/test-listen
└ [+27 more paths]
30のパスに対して事前に静的なファイルを生成したことが確認できます。実際にアクセスすると以下のように表示されます。
<div id="__next">
<div>ms stars: 2699</div>
</div>
<div id="__next">
<div>micro stars: 9128</div>
</div>
<div id="__next">
<div>test-listen stars: 115</div>
</div>
こちらも再ビルドしない限りいつアクセスしても結果は同じです。
fallback とは
getStaticPathsはpathsとfallbackが必須パラメーターです。
pathsは前述通り事前にビルドするパス対象を指定するためのもです。
fallbackは事前ビルドしたパス以外にアクセスしたときの動作を決めるものです。
false の場合
404 pageとなります。
true の場合
先程の例ではGitHubのAPIを使って30のページを事前にビルドしました。しかしZEITが管理するレポジトリは30以上存在します。別のレポジトリの名前でアクセスした場合には事前にビルドされていなくてもページを正しく表示させたいです。
その場合以下のようにfallbackを設定します。
import fetch from 'node-fetch'
+ import { useRouter } from 'next/router'
function Zeit({ name, stars }) {
+ const router = useRouter()
+ if (router.isFallback) {
+ return <div>Loading...</div>
+ }
return <div>{name} stars: {stars}</div>
}
export async function getStaticPaths() {
// 省略
+ return { paths, fallback: true } // trueに
}
export async function getStaticProps({ params }) {
// 省略
}
export default Zeit
fallbackをtrueにすることにより事前にビルドしてないパスにアクセスされても404になりません。
以下がユーザーが事前にビルドしていないパスへアクセスしたときのフローです。
-
useRouterを使い一時的にpageのfallbackバージョンを表示させる -
getStaticPropsがサーバー側で実行され静的なファイルを生成する - 生成が完了したらfallbackバージョンから切り替える
- 以後のリクエストに対しては事前にビルドしたページと同じように静的なファイルを返す
今回のケースでは取得した30件にhyper-siteが含まれていない場合には
<div id="__next">
<div>Loading...</div>
</div>
上のように一時的にLoadingが表示されます。数秒後、サーバー側でgetStaticPropsの評価が正しく終了すると以下のように表示されます。
<div id="__next">
<div>hyper-text stars: 243</div>
</div>
以後のアクセスではLoadingは表示されません。今回生成された静的なファイルを返します。
getServerSideProps
最後はgetServerSidePropsです。とは言っても殆ど解説することがありません。
これまでのgetInitialPropsと似たSSRのためのAPIです。使い心地もあまり変わらないでしょう。
異なる点は**getServerSidePropsは必ずサーバーサイドで実行**されます。
getInitialPropsはnext linkでルーティングを行った場合にはクライアントサイドで実行されていました。
一方、getServerSidePropsはnext linkでルーティングした場合にもサーバーサイドで実行するようにクライアントサイドから指示をし、実行結果が含まれるJSONファイルがサーバーから返却されます。このJSONファイルをつかってNext.jsがクライアントサイドでレンダリングを行います。
getInitialPropsは状況によって実行環境が変化するためいくつかの弊害がありました。
そのため**getInitialPropsは今後、非推奨になるようです。**
next.js/getInitialProps.md at canary · zeit/next.js
If you're using Next.js 9.3 or newer, you should use getStaticProps or getServerSideProps instead of getInitialProps.
(追記:冒頭でも記しましたが非推奨という表現は適切ではありませんでした。しかし、まずこのAPIを使うことを意識しましょう。)
まとめ
Next.jsの3つの新APIについて解説しました。
getInitialPropsを加えた4つのメソッドの実行環境とタイミングは以下の通りです。
| サーバーサイド | クライアントサイド | 実行タイミング | |
|---|---|---|---|
| getStaticProps | ◯ | ✗ | ビルド時 (+ fallback=trueならリクエストに応じて) |
| getStaticPaths | ◯ | ✗ | ビルド時 (+ fallback=trueならリクエストに応じて) |
| getServerSideProps | ◯ | ✗ | リクエストに応じて |
| getInitialProps | ◯ | ◯ | リクエストに応じて |
Next.jsをあまり使ったことがない人はSSRの印象が強いようです。この記事からもわかる通りNext.jsは9.3以降SSGの機能が大幅に強化されます。
これから導入を考えている方はSSRが本当に必要かどうか判断しSSGで実現できないか考えてみると良いかもしれません。
おまけ
(導入予定)Incremental Static Generationについて
以下のRFCについてです。
RFC: Incremental Static Generation · Discussion #11552 · zeit/next.js
現在はStatic Buildしたアプリケーションの一部に変更があった場合、アプリケーション全体を再ビルドする必要があります。
この新機能Incremental Static Generationはページ毎に再ビルドを実現するものです。
「巨大なアプリケーションになる予定なので変更毎にビルドをするのが大変そう」と感じて導入を見送ろうとしている方は要チェックです。
Next.jsでJamstack
「これJamstackでは?」と思った人も多いことでしょう。
Jamstackについて聞き馴染みの無い方は以下の記事がオススメです。
JAMstackってなに?実践に学ぶ高速表示を実現するアーキテクチャの構成 - エンジニアHub|若手Webエンジニアのキャリアを考える!
Next.jsを開発・運営するZEIT社のCEOであるGuillermo Rauch氏は去年のJSConf JPで
BUILDING AND DEPLOYING FOR THE MODERN WEB WITH JAMSTACK
というタイトルで登壇されていました。
氏の個人ブログ2019 in ReviewではJamstackについてやStaticがなぜ強力であるかについて記しています。
今後、Next.jsでJamstackなアプリケーションが増えていくのではないでしょうか。