💨

reactチュートリアル5 (Dynamic Routes)

2024/05/19に公開

NextJSの学習をする。
以下のページを翻訳し記録する。
https://nextjs.org/learn-pages-router/basics/create-nextjs-app

  1. reactチュートリアル1(create a nextjs app)
  2. reactチュートリアル2(Navigate Between Pages)
  3. reactチュートリアル3(Assets, Metadata, and CSS)
  4. reactチュートリアル4(Pre-rendering and Data Fetching)
  5. reactチュートリアル5 (Dynamic Routes)
  6. reactチュートリアル6(API Routes)
  7. reactチュートリアル7(Deploying Your Next.js App)

Dynamic Routes

ここまでブログページのインデックスを作ってきましたが、個人のブログページこんな感じは作成していません、これらのページのURLはブログデータに依存させたいです。つまり、dynamic routesを使う必要があります。

このレッスンで学ぶこと

このレッスンでは以下を学びます。

外部データに依存するページパス

前のレッスンでは、ページの内容が外部データに依存する場合について話をしました。getStaticPropcを使い、インデックスページを描画するのに必要なデータを取得します。

本レッスンでは、ページパスが外部データに依存する場合について話をします。Nextjsによって外部データに依存するパスを持った静的ページを生成することが可能となります。これによりdynamic URLsが可能となります。

Page Path Depends on External Data

どの様にDynamic Routesを使ってページを静的に生成するか

今回はブログ投稿毎にdynamic routesを作りたいです。

  • /posts/<id>のパスを持った各投稿を作りたいです。<id>はトップレベルのpostsディレクトリの下のマークダウンファイルの名前です。
  • 今回は「ssg-ssr.md」と「pre-rendering.md」になります。/posts/ssg-ssr と /posts/pre-rendering のパスを作りたいです。

手順の概要

次の手順で実現できますが、まだ作業しなくて大丈夫です。次のページでまとめて実施します。

まずpages/postsに[id].jsというページを作ります。[で始まり]で終わるページはNext.jsにおいてdynamic routesを意味します。

pages/posts/[id].js ページでは、今まで作って来たその他のページと同じ様に、投稿ページを表示するcodeを作成します。

import Layout from '../../components/layout';

export default function Post() {
    return <Layout>...</Layout>
}

ここでポイントです。このページからgetStaticPathsという非同期関数をエクスポートするのです。この関数ではidとしてあり得る値を返す必要があります。

import Layout from '../../components/layout';

export default function Post() {
    return <Layout>...</Layout>
}

export async function getStaticPaths() {
    // Return a list of possible value for id
}

最終的にgetStaticPropsをもう一度実装する必要があります。次は与えられたidのブログ投稿を取得するためです。getStaticPropsは与えられたパラメータで、idを含みます。(なぜならファイル名は[id].jsだからです)

import Layout from '../../components/layout';

export default function Post(){
    return <Layout>...</Layout>;
}

export async function getStaticPaths() {
    // Return a list of possible value for id
}

export async function getStaticProps({params}) {
    // Fetch necessary data for the blog post using params.id
}

以下に図でまとめます。

HowToStaticallyGeneratePagesWithDynamicRoutes

次ページで試してみましょう!

getStaticPathsを実装する

まずはファイルをセットアップしましょう。

  • pages/posts ディレクトリに [id].js を作ります
  • もう使わなくなったfirst-post.jsを削除します

次に pages/posts[id].js を開き、次のcodeを貼り付けます。中身は後で追加します。

import Layout from '../../components/layout';

export default function Post() {
  return <Layout>...</Layout>;
}

次に lib/posts.js を開き次に示す getAllPostIds 関数を末尾に追加します。これはpostsディレクトリ内の.mdファイルを除くファイル名のリストを返します。

export function getAllPostIds() {
  const fileNames = fs.readdirSync(postsDirectory);

  // Returns an array that looks like this:
  // [
  //   {
  //     params: {
  //       id: 'ssg-ssr'
  //     }
  //   },
  //   {
  //     params: {
  //       id: 'pre-rendering'
  //     }
  //   }
  // ]
  return fileNames.map((fileName) => {
    return {
      params: {
        id: fileName.replace(/\.md$/, ''),
      },
    };
  });
}

重要: 返ってくるリストはただの文字列ではありません。上記のコメントの様なオブジェクトの配列である必要があります。それぞれのオブジェクトはidをkeyとして持つparamsをkeyとして保つ必要があります。(idを持つ理由は、ファイル名に[id]を使っているからです。)さもないと、getStaticPathsは失敗します。

最終的にgetAllPostIds関数をインポートし、getStaticPathsで呼び出します。pages/posts[id].jsを開いてエクスポートされたPostコンポーネントの上に次のcodeをコピーしましょう。

export async function getStaticPaths() {
  const paths = getAllPostIds();
  return {
    paths,
    fallback: false,
  };
}
  • pathsにはgetAllPostIds()で返却される既知の配列を持ち、pages/posts/[id].js で定義されるparamsを含みます。paths key documentationで更に学習しましょう。
  • 今はfallback: falseは無視しましょう。後で説明します。

ほとんど完成です!しかしまだgetStaticPropsの実装が必要です。次のページで実装しましょう!

Implement getStaticProps

与えられたidの投稿を表示するために必要なデータを取ってくる必要があります。
そのため lib/posts.js を再び開き、下部に次の getPostData関数を追加します。それはidを元にした投稿データを返します。

export function getPostData(id) {
  const fullPath = path.join(postsDirectory, `${id}.md`);
  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,
  };
}

そして pages/post[id].js を開き、この行を置き換えます。

import { getAllPostIds } from '../../lib/posts';

以下のようにします。

import { getAllPostIds, getPostData } from '../../lib/posts';

export async function getStaticProps({ params }) {
  const postData = getPostData(params.id);
  return {
    props: {
      postData,
    },
  };
}

投稿ページは 投稿データを取るために getPostData 関数を getStaticPropsで使いpropsとして返します。

ここでpostDataを使って投稿コンポーネントを更新しましょう。 pages/posts/[id].js の中でエクスポートされたPostコンポーネントを次のように変更しましょう。

export default function Post({ postData }) {
  return (
    <Layout>
      {postData.title}
      <br />
      {postData.id}
      <br />
      {postData.date}
    </Layout>
  );
}

以上です!
次のページにアクセスしてみましょう。

各ページのブログデータが見られるはずです。

blog-data

素晴らしい!私達は dynamic routesの生成に成功しました!

何かがおかしいですか?

エラーが起きたら、ファイルのcodeが正しいか確認しましょう。

まだ詰まっているようならばGitHub Discussionsに気軽に聞いて下さい。GitHubにあなたのcodeをpushすれば、みんながcodeを見られるので良さそうです。

まとめ

再度やってきたことを図でまとめます。

HowToStaticallyGeneratePagesWithDynamicRoutes

まだブログのマークダウンコンテンツを表示することはしていませんので、次はそれをやりましょう。

マークダウンを表示する

マークダウンコンテンツを表示するためにremarkというライブラリを使います。まずはインストールしましょう。

npm install remark remark-html

次に lib/posts.js を開き次のimportsをファイルのトップに追加します。

import { remark } from 'remark'
import html from 'remark-html'

そして同じファイルのgetPostData()をremarkを使って更新します。

export async function getPostData(id) {
  const fullPath = path.join(postsDirectory, `${id}.md`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  // Use gray-matter to parse the post metadata section
  const matterResult = matter(fileContents);

  // Use remark to convert markdown into HTML string
  const processedContent = await remark()
    .use(html)
    .process(matterResult.content);
  const contentHtml = processedContent.toString();

  // Combine the data with the id and contentHtml
  return {
    id,
    contentHtml,
    ...matterResult.data,
  };
}

重要: remarkにawaitを使う必要があるためgetPostDataにasyncキーワードを付与しています。 async/await によって同期的にデータを取得することができます。

つまり awaitを使うためにpages/posts/[id].js の getStaticPropsを更新する必要があります。

export async function getStaticProps({ params }) {
  // Add the "await" keyword like this:
  const postData = await getPostData(params.id);

  return {
    props: {
      postData,
    },
  };
}

最終的に dangerouslySetInnerHTML を使ってcontentHtmlを表示するために pages/posts/[id].js のPostコンポーネントを更新する必要があります。

export default function Post({ postData }) {
  return (
    <Layout>
      {postData.title}
      <br />
      {postData.id}
      <br />
      {postData.date}
      <br />
      <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
    </Layout>
  );
}

以下ページにアクセスしてみましょう。

ブログコンテンツが見られるはずです。

blog-content

ほとんど完成です!次にそれぞれのページを改善しましょう。

投稿ページの改善

投稿ページに題名を付与する

pages/posts/[id].jsで、postデータを使ってtitleタグを追加しましょう。ファイルトップにnext/headのインポートを追加する必要があり、投稿コンポーネントを更新することでtiteタグを追加します。

// Add this import
import Head from 'next/head';

export default function Post({ postData }) {
  return (
    <Layout>
      {/* Add this <Head> tag */}
      <Head>
        <title>{postData.title}</title>
      </Head>

      {/* Keep the existing code here */}
    </Layout>
  );
}

日付をフォーマットする

日付をフォーマットするために date-fnsライブラリを使います。まずはインストールします。

npm install date-fns

次に components/date.js ファイルを作り、次のDateコンポーネントを追加します。

import { parseISO, format } from 'date-fns';

export default function Date({ dateString }) {
  const date = parseISO(dateString);
  return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>;
}

Note: 他のフォーマットオプションは こちらで確認できます。

では pages/posts/[id].js を開いて、Dateコンポーネントのインポートをファイルの先頭に追加してください。そしてpostData.dateを覆う形で使います。

// Add this import
import Date from '../../components/date';

export default function Post({ postData }) {
  return (
    <Layout>
      {/* Keep the existing code here */}

      {/* Replace {postData.date} with this */}
      <Date dateString={postData.date} />

      {/* Keep the existing code here */}
    </Layout>
  );
}

http://localhost:3000/posts/pre-renderingにアクセスすると**"January 1, 2020"**と記載された年月日が見られるはずです。

CSSを追加する

最後に styles/utils.module.css を使ってCSSを追加しましょう。 pages/posts/[id].js を開いて、CSSファイルのインポートを追加します。そしてポストコンポーネントを次のcodeに置き換えます。

// Add this import at the top of the file
import utilStyles from '../../styles/utils.module.css';

export default function Post({ postData }) {
  return (
    <Layout>
      <Head>
        <title>{postData.title}</title>
      </Head>
      <article>
        <h1 className={utilStyles.headingXl}>{postData.title}</h1>
        <div className={utilStyles.lightText}>
          <Date dateString={postData.date} />
        </div>
        <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
      </article>
    </Layout>
  );
}

http://localhost:3000/posts/pre-renderingにアクセスすると少し良い見た目になっています。

look-a-little-better

やりました!
次はインデックスページを洗練しましょう!

Polishing the Index Page

次に、 pages/index.js を更新しましょう。Linkコンポーネントを使ってそれぞれの投稿ページにリンクを追加する必要があります。

pages/index.js を開いて、LinkとDateのためのインポートをファイル上部に追加しましょう。

import Link from 'next/link';
import Date from '../components/date';

次に同じファイルのHomeコンポーネント下部近くで li タグを次のように更新しましょう。

<li className={utilStyles.listItem} key={id}>
  <Link href={`/posts/${id}`}>{title}</Link>
  <br />
  <small className={utilStyles.lightText}>
    <Date dateString={date} />
  </small>
</li>

http://localhost:3000, に行くとそれぞれの記事にリンクが存在しています。

blog

もし何かが動かなければcodeがこの様になっていることを確認しましょう。

以上です!
まとめに入る前にdynamic routesのコツについて話をしておきましょう。

Dynamic Routes の詳細

ここで dynamic routesについて知っておくべきいくつかの情報について説明します。

外部APIからのデータ取得やデータベース問い合わせ

getStaticPropsのようにgetStaticPathsはどの様なデータソースからもデータを取得できます。我々の例では、getAllPostIds(getStaticPathsで使われている)は外部のAPIエンドポイントから取得するかもしれません。

export async function getAllPostIds() {
    // Instead of the file system,
    // fetch post data from an external API endpoint
    const res = await fetch('...')
    const posts = await res.json();
    return posts.map((post)=>{
        return {
            params: {
                id: post.id,
            }
        }
    })
}

開発と製品

  • 開発においては(npm run dev あるいは yarn dev) getStaticPaths は全てのリクエストで実行されます。
  • 製品では getStaticPaths はビルド時に呼ばれます。

Fallback

getStaticPathsから fallback: false を返していたことを思い出してください。 これは何を意味するでしょうか?

fallback is falseの場合は getStaticPaths から返らないパスは404 pageとなります。
fallback is true の場合は getStaticPaths の挙動は変わります。

  • getStaticPathsから返るパスはビルド時にHTMLとして描画されます。
  • ビルド時に作られなかったパスは404 pageにはなりません。そのかわりに、Next.jsは後退パージョンページを表示します。
  • 裏ではNext.jsはリクエストされたパスを静的に生成します。次に同じページパスへのリクエストは作られたページを保持します。ビルド時に事前レンダリングされたページのようにです。

もし fallback is blockingだったら、新しいパスはgetStaticPropsを使ってサーバーサイドでレンダリングされます。そしてパスに対して一度だけ次のリクエストのためのキャッシュを生成します。

これは本レッスンの範囲を超えますが、fallback documentationでfallback: trueとfallback: 'blocking'について知ることができます。

全てをキャッチするルート

ダイナミックルートは(...)の様にカッコの中に3つのドットを足すことで全てのパスをキャッチするように拡張ができます。例えば

  • pages/posts[...id].js は /posts/aにマッチしますが、 /posts/a/b にも /posts/a/b/c にもマッチします。

これを実行するには、getStaticPathsでidキーの値として次のような配列を返す必要があります。

return [
    {
        params: {
            // Statically Generates /posts/a/b/c
            id: ['a', 'b', 'c'],
        },
    },
    //...
];

そして params.id はgetStaticPathsでは配列である必要があります。

export async function getStaticProps({params}){
    // params.id will be like ['a', 'b', 'c']
}

更に学習するためcatch all routes documentationに目を通しましょう。

ルーター

Next.jsのルーターにアクセスしたい場合は、next/routerからuseRouterをインポートすることで実施できます。

404 Pages

custom 404 pageをつ売りたい時は pages/404.js を作りましょう。このページはビルド時に静的に作られます。

更に学習するためError Pagesに目を通しましょう。

追加の例

getStaticPropsgetStaticPathsを図示する例をいくつか作りました。更に学習するためこれらのソースコードを見てみましょう。

以上です!
次はAPI Routesについて話します。

Discussion