Next.js と TypeScript の、getStaticProps を完全に理解する!

TwitterFacebookHatena

TL;DR

このページでは、まだまだ使い機会が多い Next.js(Pages Router) の getStaticPropsの実装方法について解説しますね。

getStaticProps の要点をまとめると「getStaticProps 関数は非同期関数で、API やデータベースからデータを取得し、取得したデータを props として返します。この関数はビルド時にページが静性に生成されるので高速に表示され、また、revalidate パラメータを使って定期的にページの再生成も可能です。」ということになります。

開発環境 バージョン
Next.js(Pages Router) 13.4.4
TypeScript 5.0.4
Emotion 11.11.0
React 18.2.0

getStaticProps とは?

getStaticPropsは、Next.js のデータフェッチング戦略の一つで、ビルド時にページに必要なデータを取得するための非同期関数です。具体的には、API からデータを取得したり、データベースからデータを取得したりする場面で使用します。

これにより、ビルド時にページが静的に生成され、その結果として生成された HTML がブラウザに送られるので、ユーザーは非常に高速なページ読み込み速度を体験できます。

それでは、簡単なgetStaticPropsの例を見てみましょう。以下は、API からデータを取得し、そのデータをページのプロパティとして使用する簡単な例です。

import { GetStaticProps } from 'next'
// 型をインポートします

import { MyData } from 'path-to-my-data-model'
// MyData 型(データモデル)をインポートします

type Props = {
  // Props 型を定義します。このコンポーネントが受け取る props の型を表します。
  data: MyData // data という名前の prop は MyData 型であることを示します。
}

export const getStaticProps: GetStaticProps<Props> = async () => {
  // getStaticProps 関数を定義します。この関数は非同期処理を含みます。
  const res = await fetch('https://my-api.com/data') // データを非同期に取得します。
  const data = await res.json() // 取得したデータを JSON 形式にパースします。

  return {
    // 取得したデータを props として返します。
    props: {
      data, // data を返します。
    },
  }
}

const MyPage = ({ data }: Props) => {
  // MyPage コンポーネントを定義します。このコンポーネントは props として data を受け取ります。
  // データを使ってページをレンダリングします。
}

ここで、getStaticProps関数の中で API からデータを取得しています。取得したデータは、propsとして返され、それらは MyPage コンポーネントに渡されます。そのため、MyPage コンポーネントの中では取得したデータを使ってページをレンダリングすることができます。

async/await

まず、非同期処理について解説します。

非同期処理とは、一つの処理(タスク)が完了するのを待たずに次の処理を進めることを指します。これにより、時間のかかる処理(例えばデータベースのクエリや外部 API へのリクエストなど)があっても、それが完了するまで他の処理を停止しないで進めることができます。

たとえば、Web ページでユーザーの操作に応答しながら背景でデータを読み込むような場合、データの読み込みは非同期に行うと良いです。非同期処理を行わないと、データの読み込みが完了するまでユーザーの操作に応答できなくなる可能性があります。

JavaScript では、非同期処理は主に以下の 3 つの方法で行われます。

  1. コールバック関数:非同期処理が完了したら呼び出される関数を設定します。
  2. プロミス:非同期処理の結果を表すオブジェクトで、成功(resolve)または失敗(reject)時の処理をチェーン(連鎖)的に記述できます。
  3. async/await:プロミスをより直感的に、あたかも同期処理のように書くための構文です。async関数内ではawaitキーワードを使って非同期処理の結果を待つことができます。

async/awaitは JavaScript の非同期処理をより簡潔で読みやすい形で記述するための構文です。これにより、非同期処理を同期処理のように直列的に書くことができます。

asyncキーワードは関数の前に置き、その関数が非同期であることを示します。これは、その関数が Promise を返すことを意味します。Promise は、値がまだ利用可能ではないが、将来的には利用可能になることを示すオブジェクトです。

awaitキーワードは、Promise が解決されるのを待つために使用します。つまり、Promise が結果を返すまで、その後のコードの実行を一時停止します。awaitasync関数内でのみ使用できます。

これらの構文を使うと、非同期のコードをあたかも同期的なコードのように読み書きでき、コードの可読性が向上します。

async/await が使われている理由

getStaticProps関数内でasync/awaitが使用される主な理由は、非同期的なデータフェッチングを行うためです。Next.js のgetStaticProps関数は、ビルド時にサーバー側で実行され、ページに必要なデータをフェッチしてプロップとして返します。このデータフェッチング操作は、外部 API からデータを取得したり、データベースからデータを読み取るなど、時間がかかる可能性があります。

これらの操作は非同期的に行われるため、JavaScript の Promise と一緒にasync/awaitを使用します。これにより、非同期処理が完了するまで待つことができ、その結果を利用してページの props を正確に生成することができます。

export async function getStaticProps() {
  // fetchは非同期関数であり、外部APIからデータを取得するために使われます
  const response = await fetch('https://api.example.com/data')

  // レスポンスをjson形式にパースするのも非同期操作です
  const data = await response.json()

  return {
    props: {
      data, // フェッチしたデータをpropsとしてページに渡します
    },
  }
}

このコードでは、fetch関数とresponse.jsonメソッドを使って非同期的にデータをフェッチし、それをページの props として返しています。これらの非同期操作が完了するまで、async/awaitを使用して処理を待機しています。

GetStaticProps

GetStaticPropsは Next.js の型です。TypeScript を使用する場合、この型はgetStaticProps関数の形状(つまり、関数が何を受け取り、何を返すべきか)を定義するために使用されます。

GetStaticPropsを使用すると、getStaticProps関数が正しい形状を持っていることを TypeScript がチェックできます。これにより、開発者はコンパイル時に関数の誤った使用を見つけることができます。

次に、公式サイトのFunctions: getStaticProps | Next.jsに書いてあるコードの解説を追ってみましょう。

getStaticProps の戻り値

getStaticProps 関数は、props、redirect、または notFound を含むオブジェクトを返し、任意で revalidate プロパティを続けて返すべきです。

props

props オブジェクトはキーと値のペアで、各値はページコンポーネントに受け取られます。渡される任意の props が JSON.stringify でシリアライズ(※1)可能な形であるべきなので、それはシリアライズ可能なオブジェクトであるべきです。

export async function getStaticProps(context) {
  return {
    props: { message: `Next.js is awesome` }, // ページコンポーネントにpropsとして渡されます
  }
}

※1 シリアライズとは、データ構造やオブジェクト状態を一連のビット列(通常は文字列)に変換するプロセスを指します。このプロセスは、メモリ内のオブジェクトの状態を、ディスクに保存したり、ネットワークを介して他の場所に送信したりするために使用されます。

たとえば、JavaScript ではオブジェクトを JSON 形式の文字列に変換するときに「シリアライズ」が行われます。これにより、オブジェクトをファイルやデータベースに保存したり、API を通じて他のコンピュータに送信したりすることができます。

revalidate

revalidate属性は、ページ再生成が可能になるまでの秒数を指定します。デフォルトは false で、再検証は行われません。

// この関数はサーバーサイドでビルド時に呼び出されます。
// 再検証が有効になっていて新たなリクエストが入った場合、
// サーバーレス関数で再度呼び出される可能性があります。
export async function getStaticProps() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  return {
    props: {
      posts,
    },
    // Next.jsは次の場合にページの再生成を試みます:
    // - リクエストが入ったとき
    // - 最大で10秒に1回
    revalidate: 10, // 秒単位
  }
}

この設定があると、Next.js は新たなリクエストが来た時や設定した秒数(この例では 10 秒)ごとに、ページを再生成しようと試みます。これにより、静的生成されたページでもデータを定期的に最新のものに更新することができます。

notFound

notFoundはブール値で、これを利用することでページが 404 ステータスと 404 ページを返すように設定することができます。notFound: trueと設定すると、以前に成功して生成されたページがあったとしても、そのページは 404 を返します。これは、例えばユーザーが作成したコンテンツが作者によって削除されたといったシチュエーションに対応するための機能です。なお、notFoundは上で説明したrevalidateの挙動に従います。

export async function getStaticProps(context) {
  const res = await fetch(`https://.../data`)
  const data = await res.json()

  // データが存在しない場合、notFoundをtrueにして404ページを返す
  if (!data) {
    return {
      notFound: true,
    }
  }

  // データが存在すればそのデータをpropsとしてページコンポーネントに渡す
  return {
    props: { data },
  }
}

この例では、特定のデータをフェッチしようとしたときに、そのデータが存在しない場合に 404 ページを返すように設定しています。

InferGetStaticPropsType

InferGetStaticPropsType は、getStaticProps 関数から props の型を推論するためのユーティリティ型です。

以下のコードは、GitHub API を使って特定のリポジトリ(この場合は Vercel の Next.js リポジトリ)の情報を取得し、そのリポジトリのスターの数をページに表示する機能を実装しています。GetStaticProps を使用することで、ビルド時にデータを取得し静的なページを生成します。これにより、ページのロード時間が短縮され、パフォーマンスが向上します。

import type { InferGetStaticPropsType, GetStaticProps } from 'next'
// InferGetStaticPropsType と GetStaticProps を next.js からインポートします。

type Repo = {
  name: string
  stargazers_count: number
}
// Repo 型を定義します。この型は GitHub リポジトリの情報を表します。

export const getStaticProps: GetStaticProps<{
  repo: Repo
}> = async () => {
  const res = await fetch('https://api.github.com/repos/vercel/next.js')
  // GitHub API から特定のリポジトリ(この場合は next.js リポジトリ)の情報を非同期に取得します。

  const repo = await res.json()
  // 取得したデータを JSON 形式にパースします。

  return { props: { repo } }
  // パースしたデータを props として返します。
}

export default function Page({ repo }: InferGetStaticPropsType<typeof getStaticProps>) {
  // Page 関数コンポーネントを定義します。このコンポーネントは props として repo を受け取ります。
  // InferGetStaticPropsType は getStaticProps 関数から props の型を推論します。

  return repo.stargazers_count
  // レンダリングされる値として、リポジトリのスターの数(stargazers_count)を返します。
}

つまり、API を取得して静的ページに変更しているだけです。

このコードは、Next.js の静的生成(Static Generation)という機能を利用しています。

Next.js では、事前にページを生成しておき(これを「静静的生成」または「ビルド時に生成」といいます)、そのページをリクエストごとに再利用することができます。これにより、サーバー側で毎回ページを生成する必要がなくなり、ページのロード時間が大幅に短縮されます。

getStaticProps 関数は、静的生成を行う際にデータを取得するための関数です。この関数はビルド時にサーバー側で実行され、その結果をページの props として返します。この props は、ページコンポーネントに渡され、ページのレンダリングに利用されます。

具体的には、このコードでは以下の処理が行われています。

  1. ビルド時に getStaticProps 関数が実行されます。
  2. この関数内で、GitHub API を使って特定のリポジトリ(Vercel の Next.js リポジトリ)の情報を非同期に取得します。
  3. 取得した情報を JSON 形式にパースし、repo という名前の props として返します。
  4. この repo は、ページコンポーネント Page に渡されます。
  5. Page コンポーネントでは、repo からスターの数 (stargazers_count) を取得し、それをページに表示します。

つまり、このコードはビルド時に GitHub API からデータを取得し、そのデータを用いて静的なページを生成しています。これにより、ユーザーがページを訪れたときにはすでにデータがページに埋め込まれているため、迅速なページの表示が可能になります。

パラメーター

名前 解説
params ダイナミックルートを使用するページのルートパラメータを含みます。例えば、ページ名が [id].js の場合、params は { id: ... } のようになります。これは getStaticPaths と一緒に使用するべきです。
preview (draftMode に対して非推奨) ページがプレビューモードの場合は true、そうでない場合は false です。
previewData (draftMode に対して非推奨) setPreviewData によって設定されたプレビューデータです。
draftMode ページがドラフトモードの場合は true、そうでない場合は false です。
locale アクティブなロケールを含みます(有効にした場合)。
locales サポートされているすべてのロケールを含みます(有効にした場合)。
defaultLocale 設定されたデフォルトのロケールを含みます(有効にした場合)。

一覧データを取得する

他に Next.js と TypeScript を使って、getStaticPropsを用いた静的生成の実装を見ていきましょう。

具体的なサンプルとして、公開 API から一覧データを取得し、それを表示するページを作成してみます。まずは全体のコードから見ていきましょう。

// pages/products.tsx
import { GetStaticProps } from 'next'
import { Product } from 'path-to-product-model'

type Props = {
  products: Product[]
}

export const getStaticProps: GetStaticProps<Props> = async () => {
  const res = await fetch('https://my-api.com/products')
  const products = await res.json()

  return {
    props: {
      products,
    },
  }
}

const ProductsPage = ({ products }: Props) => {
  return (
    <div>
      {products.map((product) => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>{product.description}</p>
        </div>
      ))}
    </div>
  )
}

export default ProductsPage

ここで、API から取得した商品の一覧データを、getStaticPropsを使って取得しています。取得したデータは、ProductsPage コンポーネントのproductsというプロパティとして渡されます。このproductsプロパティを使って、各商品の情報を表示しています。

次に、具体的にそれぞれの部分がどのような役割を果たしているかを見ていきましょう。

getStaticPropsの役割

export const getStaticProps: GetStaticProps<Props> = async () => {
  const res = await fetch('https://my-api.com/products')
  const products = await res.json()

  return {
    props: {
      products,
    },
  }
}

この部分はgetStaticPropsの定義部分で、ここで API から商品のデータを取得しています。そして取得したデータを、propsという形で返しています。これにより、このデータは Next.js によって静的に生成されるページのプロパティとして使用できます。

データを利用するコンポーネントの定義

const ProductsPage = ({ products }: Props) => {
  return (
    <div>
      {products.map((product) => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>{product.description}</p>
        </div>
      ))}
    </div>
  )
}

export default ProductsPage

この部分は、取得したデータを表示するための React コンポーネントです。productsというプロパティを受け取り、それを用いて各商品の情報を表示しています。

ダイナミックルーティングのケース

ダイナミックルーティングのケースを以下に示します。

ファイル名は [id].tsx または [id].js になります。この場合、id はダイナミックな部分を表します。これにより、URL のこの部分は任意の値になり、その値は getStaticPathsgetStaticPropscontext.params で使用できるようになります。

たとえば、あなたが次のような URL を持つページを持っているとします。

これらの各ページは、pages/blog/[id].tsx ファイルによって動的に生成されます。ここで idmy-first-postmy-second-postanother-post などになります。この id 値は getStaticPaths で生成され、getStaticProps でデータ取得時に使用されます。

このように、Next.js のダイナミックルーティングを利用すれば、大量のページを個別に作成する必要なく、同じページ構造を持つ複数のページを効率よく生成できます。

以下の例では、Next.js を使用して blog ページを作成しますが、各 blog ページはそれぞれ異なる URL を持っています。これは、例えば、https://mywebsite.com/blog/my-first-posthttps://mywebsite.com/blog/my-second-post のような URL です。これはダイナミックルーティングと呼ばれ、URL の一部が動的に変化します。

import { GetStaticPaths, GetStaticProps } from 'next'
import { useRouter } from 'next/router'

type PostProps = {
  title: string
  content: string
}

export const getStaticPaths: GetStaticPaths = async () => {
  // ここでは実際のデータソースからデータを取得します。
  // この例では、仮の固定データを使用しています。
  const posts = [
    { id: 'my-first-post', title: 'My First Post', content: 'This is my first post' },
    { id: 'my-second-post', title: 'My Second Post', content: 'This is my second post' },
    // ...他のブログ記事
  ]

  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))

  return { paths, fallback: false }
}

export const getStaticProps: GetStaticProps<PostProps> = async (context) => {
  const { id } = context.params
  // ここでidを使用して実際のデータを取得します。
  // この例では、仮の固定データを使用しています。
  const post = {
    title: 'Sample post title',
    content: 'This is the content of the post',
  }

  return { props: post }
}

const BlogPost = ({ title, content }: PostProps) => {
  const router = useRouter()

  if (router.isFallback) {
    return <div>Loading...</div>
  }

  return (
    <div>
      <h1>{title}</h1>
      <p>{content}</p>
    </div>
  )
}

export default BlogPost

このように、getStaticPaths を使用すると、静的生成が必要なパスを指定することができます。各パスはパラメーター(このケースでは id)を含むオブジェクトでなければなりません。これらのパラメーターは getStaticProps 関数で使用でき、ページのデータを取得するための入力として使うことができます。

このケースでは、getStaticProps 関数は context オブジェクトの params プロパティから id を取得し、それを使ってブログのポストを取得します。取得したデータは props としてページコンポーネントに渡され、ページをレンダリングするために使われます。

ダイナミックルーティングは非常に柔軟性があり、ブログポストのような動的なコンテンツを扱うのに適しています。

Next.js と TypeScript の、getStaticProps を完全に理解する!