Next.js 9.3の変更点
目次
先日Next.js 9.3がリリースされました。いくつか機能が追加されましたが、特に開発に影響を与えると思われるのが、getStaticProps
, getStaticPaths
, getServerSideProps
の3つの関数です。
Next.jsはSSR(Server Side Rendering)とSSG(Static Site Generation)の2つのビルド方式があります。SSRはサーバ側で実行されるためクライアントへのJavaScript送信量を抑えられるといった利点がありますが、TTFBは遅くなります。SSGは制約こそありますがHTMLがビルド時に生成されるため、高速でSEOに強いなど近年人気があります。
Next.js 9.0ではAutomatic Static Optimizationが導入され、ページ初期化時にブロック要件がなければページはSSGされます。しかし、フェッチしたデータがコンテンツになるようなブロックの発生するページも静的に書き出せればより便利です。それを成すのがgetStaticProps
とgetStaticPath
です。一方、getServerSideProps
は明示的にSSRを行います。
⚠以降コードは可能な限り公式を模範しています。データフェッチ部分のみJSONPlaceholderを使い実際に動作するコードに書き直しています。
getStaticProps(SSG)
非同期でgetStaticProps
をエクスポートするとSSGが行えます。 まずは簡易なページでSSGを試してみます。最初にページナビゲーションができるトップページを作成します。
// pages/index.js
import Link from "next/link";
const Home = () => (
<>
<ul>
<li>
<Link href="/">
<a>Home</a>
</Link>
</li>
<li>
<Link href="/blog">
<a>Post</a>
</Link>
</li>
</ul>
<h1>Home</h1>
</>
);
export default Home;
getStaticProps
内でフェッチを行うBlogコンポーネントを作成します。
// pages/blog.js
// getStaticPropsはnode環境のため、ブラウザ標準のfetchは使用できない
import fetch from "node-fetch";
function Blog({ posts }) {
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export async function getStaticProps() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
const posts = await res.json();
return {
props: {
posts
}
};
}
export default Blog;
これをビルドすると以下になりました。
/blog
は静的生成(SSG)として書き出されています。
getStaticProps + getStaticPaths(動的ルーティング)
次はgetStaticProps
で動的ルーティングされたページのSSGを試してみます。まずBlogコンポーネントでルーティング用のリンクを設けます。
// pages/blog.js
import fetch from "node-fetch";
import Link from "next/link";
function Blog({ posts }) {
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<Link href="/posts/[id]" as={`/posts/${post.id}`}>
<a>{post.title}</a>
</Link>
</li>
))}
</ul>
);
}
export async function getStaticProps() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
const posts = await res.json();
return {
props: {
posts
}
};
}
export default Blog;
href
がディレクトリ上のパスで、as
がURLバーに表示されるパス。前者のブランケットで囲んだ部分がas
の動的に変更される{$post.id}
に対応しています。
次にpages
にposts
ディレクトリを作成し、中に[id].js
を作成します。
// /pages/posts/[id].js
import fetch from "node-fetch";
const Post = ({ post }) => (
<section key={post.id}>
<h1>{post.title}</h1>
<p>{post.body}</p>
</section>
);
// (1) SSG対象となるパスのリストを定義
export async function getStaticPaths() {
// 全件数取得
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
const posts = await res.json();
// パスのリストを作成
const paths = posts.map(post => `/posts/${post.id}`);
return { paths, fallback: false };
}
// (2) 実際にSSGする関数
// paramsには上記pathsで指定した値が入る(全件ではなく1postずつ)
export async function getStaticProps({ params }) {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${params.id}`
);
const post = await res.json();
return { props: { post } };
}
export default Post;
これでbuild
およびdev
ともに実行できます。開発環境下の場合はSSGで書き出されているわけではないので、各postsページに飛ぶときはデータフェッチが起こります。
以降、要点を解説します。
getStaticPaths
SSGの対象にしたいパスを指定するのがgetStaticPaths
の役目です。戻り値にパスの一覧とフォールバック(後述)を指定します。今回パスは/posts/1
〜/posts/100
までを指定したいため、/posts
で取得できる100件分をフェッチして、結果をmap
で回してパスを生成します。
const paths = posts.map(post => `/posts/${post.id}`);
paths
は以下のようになります。
[
'/posts/1',
'/posts/2',
'/posts/3',
'/posts/4',
...
'/post/100'
]
これが書き出す動的ルーティングのパスです。ドキュメントだと、{ params: { id: 1} }
のような形式で返すように書かれていますが、配列にパスのリストを渡すだけで問題ないようです。getStaticProps
関数が受け取る引数を見るとそれがわかります。
export async function getStaticProps(ctx) {
console.log(ctx);
...
}
例えばhttp://localhost:3000/posts/3
をリクエストすると、以下が出力されます。
{ params: { id: '3' } }
getStaticProps
が受け取る引数はgetStaticPaths
から受け取るpaths
に対応するオブジェクトで、ドキュメント通りのフォーマットになっています。
fallback
getStaticPaths
の戻り値にパスと一緒に指定する論理値です。これは指定したパス以外がリクエストされた場合に、ページをどう処理するかを決めます。
- false: 返されないパスは404にする
- true: 返されないパスはリクエストされたときに生成する
ページが大量にあってビルドに時間をかけたくない場合や、ビルド時に読み込むパスをすべて把握できない場合はtrue
にします。後者はSSRを使う方が便利なケースが多いので、false
を指定することが多いと思います。
getStaticProps
最初に説明したgetStaticPaths
です。ここでは先程指定したパスを使って個々のpostデータをフェッチしています。取得したデータをprops
経由でコンポーネントに渡すには、波括弧でラップする必要があるようです。
return { props: { post } };
あとは、Postで受け取るデータをレンダリングすると、それがSSGされます。
ビルドすると、/posts/[id]
の100件分のページがSSGで生成されているのが確認できます。
getServerSideProps(SSR)
非同期のgetServerSideProps
関数をエクスポートすると、サーバサイドレンダリング(SSR)されます。この関数は従来のgetInitialProps
と使用方法や役割は大体同じです。引数にはcontextオブジェクトが格納され、直接URLを入力したときだけではなく、next/link
を使いページ遷移したときもフェッチがあればそれを行います。
先程SSGとして記述した動的ルートをSSRで書き直してみます。
// /pages/posts/[id].js
import fetch from "node-fetch";
const Post = ({ post }) => (
<section key={post.id}>
<h1>{post.title}</h1>
<p>{post.body}</p>
</section>
);
export async function getServerSideProps(context) {
// e.g. { id: 1 }
const { id } = context.query;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
return { props: { post } };
}
export default Post;
戻り値をprops
で渡すときはSSGと同様に、波括弧でラップが必要です。
上記はgetInitialProps
で以下のように記述できていました。
import fetch from "node-fetch";
const Post = ({ post }) => (
<section key={post.id}>
<h1>{post.title}</h1>
<p>{post.body}</p>
</section>
);
Post.getInitialProps = async context => {
const { id } = context.query;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
// Wait!, Look!, Attention!
return { post };
};
export default Post
getInitialProps
は後方互換を持っているので、この書き方も現状は問題なく動作します。
その他の変更点
その他、Next.js 9.3ではSassのサポート強化、ヘッドレスCMSとの変更をリアルタイムに反映できるプレビューモード、404ページの自動静的最適化などが追加されました。
非推奨になる機能はないので、Next.jsのバージョンアップ自体は問題なく行なえます。