# Next.jsのData Fetching

Next.jsのウェブサイトのData Fetching というページに沿ってやっていきます。(開いた瞬間ページ凄い長くてそっ閉じしようと思いましたが…笑)

# getStaticProps

getStaticPropsをasyncでコールするとNext.jsがビルドしてHTMLを吐き出す時によしなにやってくれますよっていうヤツ。

export async function getStaticProps(context) {
  return {
    props: {}, // propsとしてpageコンポーネントに渡される
  }
}

この context パラメーターは以下のようなキーを含むオブジェクト

  • params はDynamic Routesで使う時のアレで、例えば [id].js だったら、{ id: … } っていう形になるのだそうですが、その辺はまたDynamic Routingのドキュメントで詳しく取り扱いますとのことですが、getStaticPathsと一緒に使う感じになりますやね
  • preview はプレビューモードの時はtrueでそうじゃない時はundefinedだそうですが、こちらも詳細はPreviewのドキュメントで、とのこと
  • previewDatasetPreviewData でセットされたデータを含むらしいですが、こちらも詳細は(ry
  • locale はアクティブなロケール(if enabled)
  • locales はサポートされた全てのロケール(if enabled)
  • defaultLocale はデフォルトのロケール(if enabled)

previewとlocale周りはよく分かりませんでしたが、とりあえずは今のところは動的なルーティングのIDがparamsに入ってくる、くらいのことを知ってりゃいいのかな。

で、getStaticProps は以下を返す、と。

  • props はrequiredなオブジェクトでpageコンポートによって受け取られるけど、これはシリアライズ可能なオブジェクト
  • revalidate はオプションだけど、何秒後にページをre-generationするかっていう設定らしい。Incremental Static Regenerationっていう章で詳しく語られるとのこと
  • notFound もオプション。404を返すかどうか的な。以下の例だと、fetchしにいったけどデータが無かった場合はnotFound: trueになって、それがreturnされてページコンポーネントに渡る。
export async function getStaticProps(context) {
  const res = await fetch(`https://.../data`)
  const data = await res.json()

  if (!data) {
    return {
      notFound: true,
    }
  }

  return {
    props: { data }, // will be passed to the page component as props
  }
}

そして、notFoundは fallback: false な場合は必要なく、getStaticPahtsで返ってきたパスだけpre-renderされますよ、とのこと。

  • redirect は{destination: string, permanent: boolean} で設定できるオプション。そしてpermanentの代わりにstatusCodeというプロパティも使うことができます、と。↓はgetStaticPropsでデータが無かった場合にトップページに302なリダイレクトをする例。
export async function getStaticProps(context) {
  const res = await fetch(`https://...`)
  const data = await res.json()

  if (!data) {
    return {
      redirect: {
        destination: '/',
        permanent: false,
      },
    }
  }

  return {
    props: { data }, // will be passed to the page component as props
  }
}

そして、何やらいろいろ注意書きがあるのだけど、ビルド時のリダイレクトは許可されていないけどnext.config.jsっていうファイルでなんか出来るらしいとか、getStaticPropsの中でインポートされたやつはクライアント側ではバンドルされないとか、fetch()はgetStaticPropsの中で呼ぶのでなくてAPI用のやつをインポートすべきだとかいろいろあるみたいですが、先に進みます。

# Simpleなサンプル

// posts はビルド時に実行される getStaticProps() で渡される
function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}

// ビルド時にサーバー側で実行されるもの
// クライアント側では実行されないのでデータベースへの直接のクエリとかもOK
export async function getStaticProps() {
  // ここで外部APIを呼ぶ。データ取得用のライブラリも使っていこう的な
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // { props: { posts } }をリターンすることでBlogコンポーネントはpostsをビルド時に受け取る
  return {
    props: {
      posts,
    },
  }
}

export default Blog

# いつgetStaticProps使うの?

これは今までの繰り返しな感じなので省略

# TypeScriptではGetStaticPropが使える

nextからGetStaticPropsをインポートする感じで。 (っていうか当たり前のように出てくるけど、自分みたいにコールバック地獄してた頃に現場から足を洗ったオジサンからすると、Promiseとかasync/awaitはどこかで本とか読んで勉強されるとよろしいかと思います…)

import { GetStaticProps } from 'next'

export const getStaticProps: GetStaticProps = async (context) => {
  // ...
}

なんか型推論が〜とかってのもあるのね。InferGetStaticPropsType<typeof getStaticProps>ってやつ。

import { InferGetStaticPropsType } from 'next'

type Post = {
  author: string
  content: string
}

export const getStaticProps = async () => {
  const res = await fetch('https://.../posts')
  const posts: Post[] = await res.json()

  return {
    props: {
      posts,
    },
  }
}

function Blog({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
  // postsをPostの配列に。
}

export default Blog

# Incremental Static Regeneration

Next.js 9.5以降の機能らしいですが。

『static content can also be dynamic』って何言っとんねん?(笑)みたいな感じではありますが、一定間隔でHTMLを改めて作り直すことができるっていう機能。確かに物凄い勢いでアクセスされるサービスで、ゆうても10秒に一回とかHTML作り直すのでよければそれの方がコスト効率イイっていうのは結構ありそう。なんだろうな、、リアルタイムなゲームの大戦みたいのは無理だけど、インターネットショッピングにおける在庫の反映とかだったらOKじゃね?みたいな割り切り的な話。

function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}

// serverlessなファンクションとして再実行が有効で新しいリクエストが来た場合
export async function getStaticProps() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  return {
    props: {
      posts,
    },
    // ここで再生成する間隔を秒単位で指定
    revalidate: 1, // In seconds
  }
}

export default Blog

そして、これは fallback: true とパーフェクトにマッチする、と。記事が後から追加されても、オンデマンドでいけるからね。

これなら、、

  • スパイクがきても安定して早くコンテンツを配信できる
  • 例えばバックグラウンドのre-generationに失敗しても古いページはそのままいるわけで、オフラインになることがない
  • データベースとかバックエンドにグワっとアクセスが集中することがないので安心

# ファイルを読むのは process.cwd() を使う

getStaticPropsの中でファイルにアクセスできるけど、その場合フルパスを指定しないといけない。Next.jsのコンパイルは今いるディレクトリとは違うところにファイルを生成するので __dirname とか使えない。その代わりに process.cwd() っていうのが。

↓これ、あー、そーっすかーって感じですが、ここでも、全部ファイル読み込み処理が終わってからっていうので await Promise.all(posts) な感じで。

import { promises as fs } from 'fs'
import path from 'path'

// posts will be populated at build time by getStaticProps()
function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>
          <h3>{post.filename}</h3>
          <p>{post.content}</p>
        </li>
      ))}
    </ul>
  )
}

export async function getStaticProps() {
  const postsDirectory = path.join(process.cwd(), 'posts')
  const filenames = await fs.readdir(postsDirectory)

  const posts = filenames.map(async (filename) => {
    const filePath = path.join(postsDirectory, filename)
    const fileContents = await fs.readFile(filePath, 'utf8')

    // ここでマークダウンからHTMLに変換するとか、
    // ファイルから取得したデータをホゲホゲする

    return {
      filename,
      content: fileContents,
    }
  })

  // で、Blogコンポーネントに渡すためのreturnなんだけど、
  return {
    props: {
      posts: await Promise.all(posts),
    },
  }
}

export default Blog

# Technical details

技術的な詳細っていうことなのですが、そんなに特質すべきアレでもなさそうな感じですが、レンダリングはHTMLだけじゃなくてgetStaticPropsの結果をJSONファイルとして生成することもできるのだそうです。で、そのJSONはクライアント側で next/link とか next/router とかで使われるらしいのですが、まだあんまり分かってないですが便利そうな予感はしますねw

開発モード(next dev)だと毎回リクエスト毎にgetStaticPropsが呼ばれるそうです。

あとはPreviewモードに関しては、その名の通りというか、公開をする前にドラフトをプレビューするみたいな時にビルド時ではなくリクエスト毎にページをレンダリングしたいとかっていうので使えるそうです。

# getStaticPaths

ビルド時にHTMLを生成する用のリストを〜っていうヤツ。

export async function getStaticPaths() {
  return {
    paths: [
      { params: { ... } } // See the "paths" section below
    ],
    fallback: true or false // See the "fallback" section below
  };
}

# paths は必須

例えば以下のように returnすると、

return {
  paths: [
    { params: { id: '1' } },
    { params: { id: '2' } }
  ],
  fallback: ...
}

pages/posts/[id].js から posts/1 と posts/2 が生成される感じ。

  • ページが pages/posts/[postId]/[commentId] だとすると、paramspostIdcommentIdを保持する必要がある
  • URLにslugがある場合は['foo', 'bar']って形にすると /foo/bar という感じにできる

# fallback も必須

fallback: falseだと、getStaticPathsで返されないパスは404になる。 例えば pages/posts/[id].js が↓こんな感じ

import { useRouter } from 'next/router'

function Post({ post }) {
  const router = useRouter()

  // まだHTMLが生成されていない場合はgetStaticPropsが終わるまでこれが表示される
  if (router.isFallback) {
    return <div>Loading...</div>
  }

  // Render post...
}

// ビルド時に呼ばれる
export async function getStaticPaths() {
  return {
    // `/posts/1` と `/posts/2` だけビルド時に生成
    paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
    // 例えば `/posts/3` が追加されるようにするための設定
    fallback: true,
  }
}

// こちらもビルド時に呼ばれる
export async function getStaticProps({ params }) {
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  return {
    props: { post },
    revalidate: 1,
  }
}

export default Post

で、どんなに時に fallback: true すると便利かというと、、大量に商材を扱うeコマースサイトとか。なぜかというと事前に全商品ビルドとかするとエラいことになってしまうため。

とは言え、fallback: trueがコンテンツを更新するようなre-generateをするわけではないので、それ系の話はIncremental Static Regenerationを見てね、と。

で、fallbackがblokingだと、404にはならずHTMLのgenerateが終わってからページ全体が表示されるような感じになるっぽい。あんまり使う機会ないかもなぁ。また更新に関してはここでもIncremental Static Regenerationを、と。

# TypeScriptのGetStaticPaths

こちらもnextからimportして↓のような感じになるのだそうです。

import { GetStaticPaths } from 'next'

export const getStaticPaths: GetStaticPaths = async () => {
  // ...
}

GetStaticPropsと同様でdevの時はリクエストごとに〜ってところ。

# getServerSideProps (Server-side Rendering)

なんというか、Server-sideでリクエストごとに〜っていうだけで、あんまり変わらんのかな、と。(ザザっと流し読みしただけだけど…)

# クライアントからのデータの取得

ページでコンテンツが頻繁に更新されるとして、Static GenerationしておいてOKなところもあるような、一部分だけリアリタイムにデータを都度取りに行きたい時。

# SWR

いきなり3文字英語なんやねんって感じだけどw データを取得(fetch)するための仕掛けとしてのReact hookということで。

↓こんな感じにするとイイよってことですが、詳しくは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>
}
このエントリーをはてなブックマークに追加

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