React × Next.js 実践開発入門 #6 事前レンダリングとデータフェッチ

公開日:2022-07-11
React
Next.js

https://www.youtube.com/watch?v=NNJMOa4hDmg

この講座はYouTubeで動画形式でも用意しています。合わせてご覧ください。

はじめに

前回は、画像などのアセットや、メタデータ、CSSによるスタイリングを学習しました。今回は事前レンダリングとデータフェッチについて学習します。

事前レンダリング

事前レンダリングとは

Next.jsを用いていない普通のReactアプリケーションの場合、最初にロードした時点では何も表示されていません。この状態からJavaScriptによってDOMが組み立てられ画面表示されます。

一方Next.jsによる事前レンダリングを用いることで、最初にロードした時点で表示するHTMLを事前にサーバサイドで用意しておくことができます。これにより次のようなメリットが得られます。

  • ページが早く表示される
  • SEOに強い (静的なHTMLで内容が得られるため、クローラに解釈されやすい)

事前レンダリングによってページを作成する方法

1つ目は、静的生成という方法です。next buildコマンドによってビルド作業を行い静的なHTMLを生成します。生成された結果のファイルにユーザーがアクセスします。

2つ目は、SSR(Server-side Rendering)という方法です。リクエストを受けるたびにNext.jsが都度レンダリングを行います。

それぞれの使い分けとしては、次のようになります。

  • 静的生成
    • パフォーマンスが求められる場合
    • 情報の更新が頻繁でない場合
  • SSR
    • 常に最新の情報を事前レンダリングで反映させる必要がある場合

データを使用したページ生成

静的生成の場合について見てみましょう。Next.jsでビルドを行う際に、データベースに格納されているデータをもとにHTMLを生成します。ページが事前に作られるため、データベースに問い合わせる必要がなく直接HTMLファイルを参照できます。例えば、ブログサイトではデータベースに格納された100件の記事データを元に、100件の記事ページのHTMLを出力します。

一方、SSRの場合は、リクエストがあった際にデータベースへ問い合わせ、そのデータをもとにHTMLを生成します。

これらの違いをコードの実装を通して学習しましょう。

静的生成の実装

Markdownファイルからページ生成

実際に静的生成を実装してみましょう。データは、必ずしもデータベースである必要はなく、ここではMarkdownファイルを例として用います。APIによって外部サーバーにあるデータを取得して用いることもできます。Markdownファイルの例は次の通りです。

---
title: 'Two Froms of Pre-rendering'
date: '2020-01-01'
---

Next.js has two forms of rendering ...

これがWebページとして静的生成するための仕組みを見てみましょう。

import { getSortedPostData } from '@/lib/posts'

export async function getStaticProps() {
  const allPostData = await getSortedPostData();
  return {
    props: {
      allPostData,
    },
  };
}

このgetStaticPropsという関数は、名前の通り静的生成に使うデータを取得するための関数です。ページに反映させるためのデータをpropsとして返しています。続いてはデータを取得しているgetSortedPostData()関数を見てみましょう。

export function getSortedPostsData() {
  // Get file names under /posts
  const fileNames = fs.readdirSync(postsDirectory);
  const allPostsData: PostData[] = fileNames.map((fileName) => {
    // Remove ".md" from file name to get id
    const id = fileName.replace(/\.md$/, '');

    // Read markdown file as string
    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');

    // Use gray-matter to parse the post metadata section
    const matterResult = matter(fileContents);

    // Combine the data with the id
    return {
      id,
      ...matterResult.data,
    };
  });

  // Sort posts by date
  return allPostsData.sort((a, b) => {
    if (a.date < b.date) {
      return 1;
    } else {
      return -1;
    }
  });
}

fs.readFileSync()によってpostディレクトリ内のファイル名一覧を取得し、fs.readFileSyncでその内容を読み取っています。ここで重要な部分がmatter関数の部分です。

const matterResult = matter(fileContents);

これはimport matter from 'gray-matter'によってインポートされています。
https://github.com/jonschlinkert/gray-matterhttps://github.com/jonschlinkert/gray-matter
gray-matterを用いることによって、Markdownファイル冒頭のYAMLヘッダを解析して用いることができるようになります。YAMLヘッダにタイトルや著者、更新日などを記述しておくことで便利に扱うことができます。

gray-matterによって、YAMLヘッダに記述したdateへアクセスできるようになったため、関数から返す際にsort関数を用いて日付順に並び替えています。

コード全体は以下を参照してください。
https://github.com/redimpulz/nextjs-react-training-blog/blob/master/lib/posts.tshttps://github.com/redimpulz/nextjs-react-training-blog/blob/master/lib/posts.ts

さて、getStaticPropsで設定したページデータを利用してみましょう。

export default function Home({
  allPostsData,
}: InferGetServerSidePropsType<typeof getStaticProps>) {
  return (
    ...
    <ul>
      {allPostsData.map(({ id, date, title }) => (
        <li key={id}>
          <Link href="/posts/[id]" as={`/posts/${id}`}>
            <a>{title}</a>
          </Link>
          <br />
          <small>
            <Date dateString={date} />
          </small>
        </li>
      ))}
    </ul>
    ...
  );
}

ページコンポーネントの引数からallPostDataを取得しています。この時の型定義はInferGetServerSidePropsType<typeof getStaticProps>)となります。allPostDataからmap関数で1記事ずつを取り出し、id, date, titleの3つの値を取得してリストとして表示させています。

コード全体は以下を参照してください。
https://github.com/redimpulz/nextjs-react-training-blog/blob/master/pages/index.tsxhttps://github.com/redimpulz/nextjs-react-training-blog/blob/master/pages/index.tsx

APIからデータ取得しページ生成

次にAPIからデータを取得する手法を見てみましょう。以前も用いた犬の画像を表示するAPIを利用します。

https://dog.ceo/dog-api/https://dog.ceo/dog-api/

type Data = {
  message: string[];
  status: string;
};

export async function getSortedPostsData() {
  // Instead of the file system,
  // fetch post data from an external API endpoint
  const imageNums = 10;
  const url = `https://dog.ceo/api/breeds/image/random/${imageNums}`;
  const res = await fetch(url);
  const data: Data = await res.json();
  return data.message;
}

今回は、画像に関するデータをfetch関数で取得してgetSortedPostsDataから返しています。

ページコンポーネントの方で、これに合うような構造を作ります。

export default function Home({
  allPostsData,
}: InferGetServerSidePropsType<typeof getStaticProps>) {
  return (
    ...
      {allPostsData.map((x) => {
        <li className={utilStyles.listItem} key={x}>
          <img src={x} style={{ width: 200, height: 200}} />
        </li>
      })}
    ...
  )
}

このように、Markdownファイルの場合もAPIを用いる場合も、データ取得の部分以外の流れは同じようにして静的生成のページを作ることができます。データベースを用いる場合も同様になります。

クライアント側のレンダリング

ここまで、事前レンダリングについて学習しました。事前レンダリングと対照的なものが、クライアント側のレンダリングです。クライアント側のレンダリングの特徴は次のとおりです。

  • 事前レンダリングが不要な場合、クライアントサイドでデータを取得
  • SWRというライブラリを使用すれば、クライアントサイドのデータ取得時にキャッシュを使用できる

このように、場合によってはこのクライアント側のレンダリングを選択するケースもあります。

まとめ

次回は、Dynamic Routesという機能によって各ブログページを作っていきます。