# Next.jsのチュートリアル、その3

Next.jsのチュートリアル、その1Next.jsのチュートリアル、その2に続いて3つ目です。

# Pre-rendering and Data Fetching

今回はブログサイトを作っていくチュートリアルということでは、まずはファイルベースでやっていくけど、そのうちデータベースとかHeadless CMSとかもやってくよ的な感じ。

今回は、Next.jsのpre-rendering機能、Static GenerationとServer-side Rendering、Static Generationのデータがある場合と無い場合、getStaticPropsを使って外部データを取り込んだり、便利な使い方だったり、、みたいなことを学んでいきますよ、と。

# Pre-rendering

ってことでデータのフェッチをしていく前にNext.jsにおてとても大事なコンセプトであるPre-renderingについて。

デフォルトではNext.jsは全てのページをpre-renderします、と。これの意味するところとしては、全部のページについてHTMLを事前に作るということで、クライアント側のJavaScriptで諸々やりくりするのとも違う。Pre-renderingはパフォーマンスも早いし、SEOにも有利、とのこと。

そして、生成された各HTMLは最小限のJavaScriptしか関連させないようになっていつつ、ページがブラウザにロードされればそのページはいい感じにインタラクティブに動く、ということをhydration(ハイドレーション)と呼ぶんだそうです。

# Pre-renderingの詳細

以下の手順でやってみると、

  • JavaScriptをブラウザで無効にする

Disable

  • ページにアクセスしてみる

アクセス

確かに、JavaScriptが無い状態でもちゃんと画面が表示されています。プレーンなReactアプリだったらこうはいかないのだそうで。

Hydrationというのは、HTMLが最初に表示されてReactのコンポーネントがイニシャライズされると、以前のLinkコンポーネントのようにブラウザに依存せずにインタラクティブに動作する的な振る舞い。そもそも、もし、Pre-renderingでなければ何も画面に表示されまへん、と。

# Pre-renderingの2つの方法

Static GenerationとServer-side Renderingというのがありまして、と。

  • Static Generationはビルド時にHTMLを作るヤツ
  • Server-side Renderingはリクエスト毎にHTMLを作るヤツ

また、Static Generationであっても開発モード(npm run dev)の場合は全てのページはリクエスト毎にレンダリングされます、と。

# ページ毎に選択できる

Next.jsではページ毎にStatic GenerationとServer-side Renderingを選択できますよ、と。これをhybrid(ハイブリッド)と呼ぶそうですが、ほとんどのページをStatic Generationにして、その他をServer-side Renderingにするとか。

オススメはデータがあってもなくてもStatic GenerationでCDNにスタティックなファイルを置いておくだけで良いので早いし良いよ、と。

  • マーケティングなランディングページ
  • ブログ記事
  • eコマースの商品情報
  • ヘルプサイトとかドキュメントサイト

この辺は使い所な感じだそうで。ってか大体Static Generationで良さそうだけど、常にup-to-dateが求められるところだったら、Server-side Renderingを使うか、もしくはpre-renderingをスキップしてクライアント側のJavaScriptで更新データを取りに行くような実装にすると良い。

ってことで、今回のレッスンはStatic Generationにフォーカスしつつ、データの扱いをやっていく感じ。

# Pre-rendering と データのフェッチ

HTMLを生成するのに外部データにアクセスする必要がある時。例えばファイルシステムにアクセスしたり、外部のAPI呼んだり、データベースにクエリしたり。Next.jsはout-of-the-boxでStatic Generation with dataできるよ、と。

ってことで、getStaticPropsを使ったデータの取得。

Next.jsではgetStaticPropsというasyncファンクションを使って以下のようにやりくりできます、と。

export default function Home(props) { ... }

export async function getStaticProps() {
  // ファイル、API、データベースなど外部のソースからデータを取得
  const data = ...

  // `props` のvalueがHomeコンポーネントに渡る
  return {
    props: ...
  }
}

でもって、開発モードの場合はリクエスト毎にgetStaticPropsが実行されますよ的な話があって、実際やってみましょう。

# ブログのデータ

マークダウンファイルに書かれたブログデータを使ってやっていく。

  • postsというディレクトリをトップレベルディレクトリに作る(以前作ったpages/postsではなく)
  • postsの中はpre-rendering.mdssg-ssr.mdという名前のファイルを置く

(なんかチョイチョイSSGっていうのが出てくるというか、残ってるというか、だけど、昔はStatic Site Generationって呼んでた名残なのかなぁ…)

ってことで、posts/pre-rendering.mdの方は↓こんな感じ。

---
title: 'Pre-renderingの2つのやり方'
date: '2021-04-30'
---

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 lets 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: 'Static Generationを使う時 v.s. Server-side Renderingを使う時'
date: '2021-05-01'
---

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.

マークダウンのメタデータのtitledateはYAML Front Matterと呼ばれるやるでgray-matterってライブラリでパース出来ます、と。

ってことで、さっそくこのデータをgetStaticPropsを使って取得してみましょう的な。これにはpages/index.jsにコードを書いていく感じ。やることは

  • markdownをパースしてtitledateとファイル名(idとして使う)を取得
  • 日付順にリスト表示する

ってことで、まずは gray-matter のインストールから

$ npm install gray-matter

added 10 packages, and audited 287 packages in 2s

44 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

でもって、これをつかってファイルからデータを取得するのをライブラリ化する感じにして、トップレベルディレクトリにlibというのを作ってそこにposts.jsというのを作っていきます、と。

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

const postsDirectory = path.join(process.cwd(), 'posts')

export function getSortedPostsData() {
  // /posts 配下にあるファイル名を取得
  const fileNames = fs.readdirSync(postsDirectory)
  const allPostsData = fileNames.map(fileName => {
    // ".md" をファイル名から取り除いてidに代入
    const id = fileName.replace(/\.md$/, '')

    // 文字列としてmarkdownファイルを読み取る
    const fullPath = path.join(postsDirectory, fileName)
    const fileContents = fs.readFileSync(fullPath, 'utf8')

    // gray-matter を使ってメタデータをパース
    const matterResult = matter(fileContents)

    // idとパースしたデータを返す
    return {
      id,
      ...matterResult.data
    }
  })
  // dateでソート
  return allPostsData.sort((a, b) => {
    if (a.date < b.date) {
      return 1
    } else {
      return -1
    }
  })
}

(なんか、このdateでソートしてるところのallPostsData.sortっていつ呼ばれるんだろうか…?)

でもって、pages/index.jsの中のgetStaticProps用に上記のgetSortedPostsDataをimportしましょうね、と。

ってことで、pages/index.js

import Head from 'next/head'
import Layout, { siteTitle } from '../components/layout'
import utilStyles from '../styles/utils.module.css'
import { getSortedPostsData } from '../lib/posts'

export async function getStaticProps() {
  const allPostsData = getSortedPostsData()
  return {
    props: {
      allPostsData
    }
  }
}

export default function Home() {
  return (
    <Layout home>
      <Head>
        <title>{siteTitle}</title>
      </Head>
      <section className={utilStyles.headingMd}>
        <p>Yo!!</p>
        <p>
          (This is a sample website - you’ll be building a site like this on{' '}
          <a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
        </p>
      </section>
    </Layout>
  )
}

(少し長いけど、現時点でのコードのスナップショット)

そして、Homeコンポーネントに新しく<section>タグを加えて、以下のように。

import Head from 'next/head'
import Layout, { siteTitle } from '../components/layout'
import utilStyles from '../styles/utils.module.css'
import { getSortedPostsData } from '../lib/posts'

export async function getStaticProps() {
  const allPostsData = getSortedPostsData()
  return {
    props: {
      allPostsData
    }
  }
}

export default function Home({ allPostsData }) {
  return (
    <Layout home>
      <Head>
        <title>{siteTitle}</title>
      </Head>
      <section className={utilStyles.headingMd}>
        <p>Yo!!</p>
        <p>
          (This is a sample website - you’ll be building a site like this on{' '}
          <a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
        </p>
      </section>
      <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>
  )
}

(なんかおじさん的にはさっきのsortもそうだけど {allPostsData.map(({ id, date, title }) => ( これでいい感じに回せちゃうのもなんかしっくりこないんだけど…笑)

と、そんなこんなで↓ちゃんと出ました〜

Posts

# getStaticPropsの詳細

上記ではファイルシステムからデータを取得してきたけど↓のようにAPIを使ってデータを取得することもできますよ、と。

export async function getSortedPostsData() {
  // ファイルシステムからデータを取得するのではなく、
  // 外部のAPIエンドポイントからデータを取得する
  const res = await fetch('..')
  return res.json()
}

また、Next.jsは自身のfetchファンクションをクライアント側/サーバー側ともに持っているため、importはしなくてもOKとのこと。

データベースについても同様に↓のように。

import someDatabaseSDK from 'someDatabaseSDK'

const databaseClient = someDatabaseSDK.createClient(...)

export async function getSortedPostsData() {
  // SQLでデータベースからデータを取得する
  return databaseClient.query('SELECT posts...')
}

getStaticPropsはサーバー側でのみ動作し、JavaScriptのバンドルすらクライアントのブラウザには含まれない。なのでここに書かれたSQLはブラウザから参照することはできない。

開発環境とプロダクションっていうところで、今まで使ってきた開発環境のnpm run devだと毎回getStaticPropsが呼ばれるけど、プロダクションではビルド時にしか呼ばれません、と。ただし、fallbackを使うとこれをハンスすることができるのだそうで。

また、ビルド時に動くことを前提にしているので、リクエスト時に来るようなクエリパラメータやHTTPのヘッダを使うことはできません。

でもって、getStaticPropsはpageからしかexportすることはできなくて、それはReactがページをレンダリングする時に全てのデータが必要、というところから来るのだそうです。

んじゃ、リクエストの時のデータをフェッチする必要があったら?っていうような場合はServer-side Renderingとかpre-renderingをスキップするとか。んま、その辺の戦略はまた次で。

# リクエスト時のデータ取得

Server-side RenderingにおいてはgetServerSidePropsというファンクションが使えるよ、と。(getStaticPropsはあくまでもStatic Generation)

んま、実際今回は使わないけど、実装としては以下のような感じ。

export async function getServerSideProps(context) {
  return {
    props: {
      // props for your component
    }
  }
}

getServerSidePropsはリクエスト時に呼ばれるので(context)にはリクエスト時のパラメータが入ってきますよ、と。そして、至るところに出てくる気がするけど、これやると遅いし、CDNにキャッシュできないからね、、的な話。

ってことで、もし、pre-renderしなくてイイんだったら、Client-side Renderingという戦略を取ることもできますよ、と。

これは、、

  • 外部データを使わないところはpre-renderingしておいて
  • ページがロードされた時にクライアントのJavaScriptを使って必要な部分を取りに行く、と。

このアプローチはユーザーダッシュボードとか、プライベートでユーザー固有でSEOとか意識する必要がなく、pre-renderしなくてもいい時に有効。そして、データは頻繁に更新されて、最新の情報をリクエスト時に取得する必要がある時。

# SWR

Next.jsはデータを取得する用のReeact hookを作りました、と。クライアント側でデータを取得する時にはこれを使うのがオヌヌメ、と。caching, revalidation, focus tracking, refetching on interval,,,とかっていう多機能な感じなのだそうで。

確かにパッと見ただけで便利そうや↓

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>
}

ってことで、Pre-rendering and Data Fetchingは以上で、次回はDynamic Routesよん。

このエントリーをはてなブックマークに追加

Algolia検索からの流入のみConversionボタン表示