その4 Pre-rendering and Data Fetching
用語
Pre-rendering
Data Fetching
- 文字のまま。データをフェッチするという意味。
- つまり、データを取り込む、どこかからデータをとってくるといった感じ。
4-1. Pre-rendering and Data Fetching
- 最終的にブログを作成したいが、現時点ではブログのコンテンツがない状態
- このレッスンでは、外部のブログデータをアプリに取り込む方法を学ぶ
- このチュートリアルでは、ブログのコンテンツをファイルシステムに格納する
- コンテンツが他の場所(データベースやヘッドレスCMSなど)に格納されている場合でも同様に実装できる
このレッスンで学ぶこと
- このレッスンでは、以下について学ぶ
- Next.jsの
pre-rendering
機能について - 二種類のpre-rendering方式:
Static Generation
とServer-side Rendering
- データありとデータなしの
Static-Generation
getStaticProps
を使用した、インデックスページへの外部ブログデータのインポート方法getStaticProps
に関するいくつかの有益な情報
- Next.jsの
4-2. Setup
- 前のレッスンから続けて受講する場合は、このページを読み飛ばすことができます。
4-3. Pre-rendering
データの取り込みについて話す前に、Next.jsの最も重要な概念のひとつである
Pre-rendering
について説明するデフォルトでは、Next.jsはすべてのページを事前レンダリングする
事前レンダリングとは
- クライアントサイドのJavaScriptですべてのHTML生成を行うのではなく、Next.jsがあらかじめ各ページのHTMLを生成しておくことを意味する
- パフォーマンスとSEOの向上につながります。
生成された各HTMLは、そのページに必要な最小限のJavaScriptコードと関連付けられている
- ブラウザがページを読み込むと、そのJavaScriptコードが実行され、ページが完全にインタラクティブになる
- このプロセスは、
Hydration
と呼ばれる
プリレンダリングが行われていることを確認する
プリレンダリングが行われているかどうかは、以下の手順で確認することができます。
- ブラウザのJavaScriptを無効にし(Chromeの場合はこちら)、...
- このページ(このチュートリアルの最終結果)にアクセスしてみてください。
JavaScriptなしでアプリがレンダリングされていることがわかるはずです。
- これはNext.jsがアプリを静的なHTMLにプリレンダリングしているためで、JavaScriptを実行せずにアプリのUIを見ることができます。
注意:上記の手順をlocalhostで試すこともできますが、JavaScriptを無効にするとCSSが読み込まれません。
- アプリがプレーンな React.js アプリ(Next.js を含まない)の場合、プリレンダリングがないため、JavaScript を無効にするとアプリが表示されなくなります。たとえば
- ブラウザのJavaScriptを有効にして、このページをご覧ください。これは、Create React Appで構築したプレーンなReact.jsのアプリです。
- ここで、JavaScriptを無効にして、もう一度同じページにアクセスしてみてください。
- アプリは表示されなくなり、代わりに "You need to enable JavaScript to run this app." と表示されます。これは、アプリが静的なHTMLにプリレンダリングされていないためです。
まとめ:プリレンダリングとプリレンダリングなしの比較
簡単に図式化すると、次のようになります。
- Next.jsありの場合
Initial Load: 事前読み込みされたHTMLが表示される
↓ JavaScriptファイル読み込み
Hydration: React要素が初期化され、アプリがインタラクティブになる
例えば、インタラクティブな要素である<Link />
があった場合、JSファイル読み込み後にアクティブになる
- Next.jsを使用しない素のReact.jsアプリの場合
Initial Load: 画面には何も描画されない
↓ JavaScriptファイル読み込み
Hydration: React要素が初期化され、アプリがインタラクティブになる(素のReactにはLinkコンポーネントはない)
4-4. Two Forms of Pre-rendering
- 事前レンダリングには次の2種類があります。
※留意事項
- 開発環境(
npm run dev
実行時)においては、開発しやすくするために①, ②に関わらず、リクエスト毎にページは事前レンダリングされる。 本番環境においては、①の方式を採用していれば、ビルド時に一度だけ事前レンダリングが行われる。
Next.jsでは、各ページに対して①, ②のどちらの方式を採用するか選択することができる
- 一般的な構成ではほとんどのページが①Static Generation、その他のページを②Server-side Renderingで実現される
- 一度ビルドすればCDNから提供され、各リクエストに対する応答がより高速であるということから可能な限り①を使うことが推奨される
①Static Generationが適している具体なページの例としては下記がある
- マーケティングページ
- ブログ投稿ページ
- Eコマース製品の一覧ページ
- ヘルプページやドキュメント
- 逆に、頻繁に更新されるデータを表示するページやリクエスト毎にコンテンツが変化するような場合は②を採用しましょう。
- 次のレッスンでは、①Static Generationに焦点を当てて、データを伴う場合、データを伴わない場合について解説していきます。
4-5. Static Generation with and without Data
- Static Generationはデータを伴う場合とデータを伴わない場合に分けて考えることができる
ここまでに作成したページは、外部データを取得しないもの
- このようなページではプロダクション環境向けにアプリをビルドした時に自動的に静的にHTMLが生成されることになります。
一方で、データ取得した後でなければ描画できないページも考えられる
- 例えば、ビルド時に、ファイルシステムや外部API、データベースなどからデータを取得するような場合だ
- Next.jsでは、データを伴うページの静的生成(Static Generation with data)にも対応できるようになっている
Static Generation with Data using getStaticProps
さて、データを伴うページの静的生成は、どのようにして動くのでしょうか?
- Next.jsでは、ページコンポーネントと一緒に
getStaticProps
という非同期関数(async function)もエクスポートすることで実現されます。
- Next.jsでは、ページコンポーネントと一緒に
動作としては、
getStaticProps
がプロダクション環境向けのビルド時に実行され- 関数内で、外部データをfetchして、そのデータをpropsとしてページに渡せます
export default function Home(props) { ... } export async function getStaticProps() { // ファイルシステム、API、DBなどからの外部データを取得する const data = ... // `props`をキーとする値が`Home`コンポーネントに渡される return { props: ... } }
[プログラムの要点]
- async functionとしてgetStaticPropsをexportして、その中で外部データを取得して、propsとしてリターンすると、Home
componentに渡すことができる
基本的に、
getStaticProps
はNext.jsに次のように伝えます。- 「おい、このページにはいくつかのデータ依存性があるぞ。だから、ビルド時にこのページをプリレンダリングするときは、最初にそれらを解決しておけよ!」。
注意: 開発モードでは、
getStaticProps
は各リクエストで実行されます。
getStaticPropsを使ってみよう
- 実際にやってみるとわかりやすいので、次のページからは
getStaticProps
を使ってブログを実装していきます。
4-6. Blog Data
シンプルなブログのアーキテクチャを作成する
マークダウンファイルの作成
- ルートフォルダに
nextjs-app/posts/
という新しいディレクトリを作成する(これはpages/posts
とは別物) nextjs-app/posts/
の中に、pre-rendering.md
とssg-ssr.md
という2つのファイルを作成する
さて、以下のコードをposts/pre-rendering.md
にコピーしてください。
--- title: 'Two Forms of Pre-rendering' date: '2020-01-01' --- Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. - **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. - **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. Importantly, Next.js let's you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
そして、以下のコードをposts/ssg-ssr.md
にコピーしてください。
--- title: 'When to Use Static Generation v.s. Server-side Rendering' date: '2020-01-02' --- We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request. You can use Static Generation for many types of pages, including: - Marketing pages - Blog posts - E-commerce product listings - Help and documentation You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation. On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request. In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
- Note
gray-matterのインストール
- まず、各マークダウンファイルのメタデータをパースするためのgray-matterをインストールする
npm install gray-matter
ファイルシステムを読み込むユーティリティ関数の作成
- ファイルシステムからデータをパースするための、次の処理を行えるユーティリティ関数を作成する
import fs from 'fs'; // Note: ファイルシステムからファイルを読み込むためのモジュール import path from 'path'; // Note: ファイルのパスを操作するためのモジュール import matter from 'gray-matter'; // Note: マークダウンファイルのメタデータをパースするためのライブラリ const postsDirectory = path.join(process.cwd(), 'posts'); export function getSortedPostsData() { // ディレクトリ"/posts"配下のファイル名取得する const fileNames = fs.readdirSync(postsDirectory); const allPostsData = fileNames.map((fileName) => { // ファイル名をidとして扱うため、不要となる拡張子".md"をファイル名から削除する(正規表現を使用している) const id = fileName.replace(/\.md$/, ''); // マークダウンファイルを文字列として読み込む const fullPath = path.join(postsDirectory, fileName); const fileContents = fs.readFileSync(fullPath, 'utf8'); // 投稿のメタデータを解析するために"gray-matter"を使う const matterResult = matter(fileContents); // 取得したidとデータを紐付ける return { id, ...matterResult.data, }; }); // 投稿日でソートする return allPostsData.sort(({ date: a }, { date: b }) => { if (a < b) { return 1; } else if (a > b) { return -1; } else { return 0; } }); }
ブログデータの取得
- ブログデータが解析されたので、それをインデックスページ (
pages/index.js
) に追加する必要がある - これを行うには、Next.jsのデータ取得メソッドである
getStaticProps()
を使用する - 次のセクションでは、
getStaticProps()
の実装方法について学びます。
4-6 まとめ
- DBの位置付けとしてこのチュートリアルではマークダウンファイルを用意した
- これらのファイルは、メタデータ(日付、タイトル)を含んでいる
- マークダウン(のメタデータ部分)を解析するために
gray-matter
をnpm install
した - 解析処理を行うための関数
getSortedPostsData
をエクスポートした ここまでのステップでマークダウンファイルから
pages/index.js
に掲載するidとデータを紐付けた内部データを扱えるようになった次のステップでは、Next.jsのデータ取得関数
getStaticProps()
を利用する方法を学習する
4-7. Implement getStaticProps
Next.jsのpre-rendering(事前レンダリング)には二通りあり、これらの違いは"HTMLがいつ生成されるか"にある
① Static Generation (静的生成)
- HTMLをビルド時に生成する方式で、リクエスト毎に再利用される
② Server-side Rendering (サーバーサイドレンダリング)
- HTMLを各リクエスト毎に毎回生成する方式
※ Next.jsにおいては、多くのページを①で実現し、その他残りのページを②で実現することで描画の効率を高めている
静的生成(getStaticProps()
)を利用する
- さて、
getSortedPostsData
のimportを追加して、pages/index.js
のgetStaticProps
の中で呼び出す必要があります。 pages/index.js
をエディタで開き、エクスポートされたHome
コンポーネントの上に以下のコードを追加します。
import { getSortedPostsData } from '../lib/posts'; export async function getStaticProps() { const allPostsData = getSortedPostsData(); return { props: { allPostsData, }, }; }
getStaticProps
でprops
オブジェクトの中にallPostsData
を返すことで、ブログの記事がpropとしてHomeコンポーネントに渡されることになります。- これで、以下のようにブログ記事にアクセスすることができます。
export default function Home ({ allPostsData }) { ... }
- ブログ記事を表示するために、Homeコンポーネントを更新し、自己紹介のあるセクションの下に、データを含む別の
タグを追加しましょう。 props
を()
から({ allPostsData })
に変更することを忘れないでください。
export default function Home({ allPostsData }) { return ( <Layout home> {/* Keep the existing code here */} {/* Add this <section> tag below the existing <section> tag */} <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}> <h2 className={utilStyles.headingLg}>Blog</h2> <ul className={utilStyles.list}> {allPostsData.map(({ id, date, title }) => ( <li className={utilStyles.listItem} key={id}> {title} <br /> {id} <br /> {date} </li> ))} </ul> </section> </Layout> ); }
これで、
http://localhost:3000
にアクセスすれば、ブログデータが表示されるはずです。おめでとうございます。外部データを(ファイルシステムから)取得し、このデータでインデックスページをプリレンダリングすることに成功しました。
getStaticProps()
Markdownファイルの解析
↓
ページコンポーネントにpropsとして投稿データの配列を渡す
↓
インデックスページにブログ投稿のリストを表示する
- 次のページでは、getStaticPropsの使い方のコツについて説明します。
4-8. getStaticProps Details
getStaticPropsの詳細
- getStaticPropsについて知っておくべき重要な情報の紹介
外部APIやデータベースを取得する
lib/posts.js
では、ファイルシステムからデータを取得するgetSortedPostsData
を実装しています。- しかし、あなたは外部APIエンドポイントのような他のソースからデータをフェッチすることができ、それはうまく動作します。
export async function getSortedPostsData() { // Instead of the file system, // fetch post data from an external API endpoint const res = await fetch('..'); return res.json(); }
注意:Next.jsはクライアントとサーバーの両方でfetch()をポリフィルしています。インポートする必要はありません。
- また、直接データベースに問い合わせることもできます。
import someDatabaseSDK from 'someDatabaseSDK' const databaseClient = someDatabaseSDK.createClient(...) export async function getSortedPostsData() { // Instead of the file system, // fetch post data from a database return databaseClient.query('SELECT posts...') }
- これは、
getStaticProps
がサーバーサイドでのみ実行されるためです。 - クライアントサイドでは決して実行されません。ブラウザ用のJSバンドルに含まれることもありません。
- つまり、データベースへの直接問い合わせのようなコードを、ブラウザに送信することなく書くことができるのです。
開発環境と本番環境
- 開発環境(
npm run dev
またはyarn dev
)では、getStaticProps
はすべてのリクエストで実行されます。 - 実運用環境では、
getStaticProps
はビルド時に実行されます。 - しかし、この動作は
getStaticPaths
が返すフォールバックキーを使って拡張することができます。 - これはビルド時に実行されるものなので、 クエリパラメータや HTTP ヘッダのようなリクエスト時にしか使用できないデータは使用できません。
ページ内でのみ許可される
getStaticPropsは、ページからしかエクスポートできません。ページ以外のファイルから書き出すことはできません。
この制限の理由の1つは、Reactはページがレンダリングされる前に必要なデータをすべて持っている必要があるためです。
リクエスト時にデータを取得する必要がある場合はどうすればよいですか?
静的生成はビルド時に一度だけ行われるため、頻繁に更新されるデータや、ユーザーのリクエストごとに変更されるデータには適していません。
このような、データが変化する可能性がある場合は、サーバーサイド・レンダリングを使用することができます。
- サーバーサイド・レンダリングについては、次のセクションで詳しく説明します。
4-8 まとめ
getStaticProps
がサーバサイドで実行されるため、ファイルシステムやデータベースからの情報の読み込みを実現できる- 開発環境では、
getStaticProps
はリクエスト毎に実行されるが、本番環境ではビルド時のみ実行される(ただし、fallback
キーを使って拡張可能) getStaticProps
はページファイルからのみエクスポートできる- 頻繁に更新が必要なページにはこの手法は向いていないため、以降に紹介される
Server-sdie Rendering
を利用する- 言い換えると、ビルド時ではなく、リクエスト時にデータの取得の必要があるならば
Server-sdie Rendering
を利用すべき
- 言い換えると、ビルド時ではなく、リクエスト時にデータの取得の必要があるならば
4-9. Fetching Data at Request Time
ビルド時ではなく、リクエスト時にデータを取得する必要がある場合は、サーバーサイドレンダリングを試すことができます。
サーバーサイド・レンダリングを使用するには、ページからgetStaticPropsの代わりにgetServerSidePropsをエクスポートする必要があります。
getServerSidePropsの使用法
getServerSidePropsのスターターコードです。このブログの例では必要ないので、実装しないことにします。
export async function getServerSideProps(context) { return { props: { // props for your component }, }; }
getServerSidePropsはリクエスト時に呼び出されるため、そのパラメータ(context)にはリクエスト固有のパラメータが含まれます。
getServerSideProps は、リクエスト時にデータを取得しなければならないページをプリレンダリングする必要がある場合にのみ、使用する必要があります。TTFB (Time to First Byte) は getStaticProps よりも遅くなります。なぜなら、サーバーはリクエストごとに結果を計算しなければならず、その結果は特別な設定なしにCDNによってキャッシュされることができないからです。
クライアントサイドレンダリング
データをプリレンダリングする必要がない場合は、次の戦略(Client-side Renderingと呼ばれる)を使用することもできます。
外部データを必要としないページの部分を静的に生成(プリレンダリング)します。 ページが読み込まれたら、JavaScriptを使用してクライアントから外部データを取得し、残りの部分にデータを入力します。
この方法は、たとえば、ユーザーのダッシュボード・ページで有効です。ダッシュボードはプライベートなユーザー固有のページなので、SEOは関係なく、ページがプリレンダリングされる必要もありません。データは頻繁に更新されるため、リクエスト時にデータを取得する必要があります。
SWR
Next.jsの開発チームは、SWRと呼ばれるデータフェッチ用のReactフックを作成しました。クライアントサイドでデータを取得する場合は、これを強くお勧めします。SWRは、キャッシュ、再バリデーション、フォーカストラッキング、インターバルでのリフェッチなどを処理します。ここでは詳細を説明しませんが、使用例を示します。
import useSWR from 'swr'; function Profile() { const { data, error } = useSWR('/api/user', fetch); if (error) return <div>failed to load</div>; if (!data) return <div>loading...</div>; return <div>hello {data.name}!</div>; }
詳しくは、SWRのドキュメントをご覧ください。
以上です。 次のレッスンでは、ダイナミックルートを使って各ブログ投稿のページを作成します。