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されます。しかし、フェッチしたデータがコンテンツになるようなブロックの発生するページも静的に書き出せればより便利です。それを成すのがgetStaticPropsgetStaticPathです。一方、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;

これをビルドすると以下になりました。

SSGのビルド結果

/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}に対応しています。

次にpagespostsディレクトリを作成し、中に[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で生成されているのが確認できます。

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のバージョンアップ自体は問題なく行なえます。