React × Next.js 実践開発入門 #6 事前レンダリングとデータフェッチ
この講座は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-matter
gray-matterを用いることによって、Markdownファイル冒頭のYAMLヘッダを解析して用いることができるようになります。YAMLヘッダにタイトルや著者、更新日などを記述しておくことで便利に扱うことができます。
gray-matterによって、YAMLヘッダに記述したdate
へアクセスできるようになったため、関数から返す際にsort
関数を用いて日付順に並び替えています。
コード全体は以下を参照してください。https://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.tsx
APIからデータ取得しページ生成
次にAPIからデータを取得する手法を見てみましょう。以前も用いた犬の画像を表示する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という機能によって各ブログページを作っていきます。