Next.js と TypeScript で、Pages と Layout を学ぶ

TwitterFacebookHatena

TL;DR

このページでは、Next.js(Pages Router) の「ページとレイアウト」の実装方法について解説しますね。一言でいうと「ページとレイアウト」とは、Web アプリケーションの各画面とその構造を定義するための Next.js の核心的な仕組みです。

この記事は、Next.js の公式ドキュメンテーションを参考に解説しています。特に、Pages and Layoutsのセクションを基に、その内容を理解しやすく解釈し直し、さらに詳細な説明を加えています。

この記事は、Next.js の公式ドキュメンテーションの素晴らしい内容を尊重し、その知識を広めることを目指しています。そのため、この記事の内容は、公式ドキュメンテーションの内容をそのまま意訳するのではなく、それを基に私たち自身の理解と経験を元にした解釈と説明を加えています。

もし、より詳細な情報や公式の説明を求める場合は、Next.js の公式ドキュメンテーションを直接ご覧いただくことを強く推奨します。この記事の解説はあくまで参考の一つであり、公式ドキュメンテーションには、より詳細な情報や最新の更新情報が含まれています。

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

「ページとレイアウト」 とは?

最初に「ページ」と「レイアウト」の概念について説明します。

「ページ」は Next.js アプリケーション内の個々の画面を表現します。これは、ウェブサイトの HTML ファイルに相当します。Next.js では、pages ディレクトリ内の各 React コンポーネントが一つのページとして扱われます。たとえば、pages/index.tsx はアプリケーションのホームページに対応します。また、pages/about.tsx/about パスのページを表します。

一方、「レイアウト」はページの共通の構造を表現します。これはヘッダーやフッターなど、複数のページで共有するコンポーネントを含むことができます。レイアウトは、ページの一部として直接実装することもできますが、より再利用性を高めるためには、レイアウトを個別のコンポーネントとして定義し、ページでインポートして使用することが一般的です。

ルートについて

ルートについて解説します。

インデックスルート

ルーターは、「index」という名前のファイルを自動的にそのディレクトリのルート(最上位)に向けてルーティングします。

たとえば、あなたが pages/index.js という名前のファイルを持っているとします。これは、ウェブサイトのホームページ(ルート URL、つまり www.yoursite.com/)に自動的にマップされます。同様に、pages/blog/index.js というファイルは www.yoursite.com/blog/ にマップされます。つまり、「index」はディレクトリの「ホームページ」を表していると言えます。

pages/
├── index.js  →  / (ルート)
└── blog/
    └── index.js  →  /blog (ブログのルート)

ネストされたルート

ルーターは、ネスト(入れ子)されたファイルをサポートしています。ネストされたフォルダー構造を作成すると、ファイルはまだ同じ方法で自動的にルーティングされます。

これは、具体的には以下のようなことを意味します。例えば、pages/blog/firstPost.jsというファイルを作成したとき、これは自動的にwww.yoursite.com/blog/firstPostという URL にマップされます。つまり、フォルダとファイルの構造は、ウェブサイトの URL 構造に直接マッピングされます。

pages/
├── index.js  →  / (ルート)
├── blog/
│   ├── index.js  →  /blog (ブログのルート)
│   └── first-post.js  →  /blog/first-post (ブログの中の最初の投稿)
└── dashboard/
    └── settings/
        └── username.js  →  /dashboard/settings/username

動的ルートを持つページ

動的ルートというのは、URL の一部が動的に変化するページのことです。例えば、ブログの投稿を表現するページは、/blog/hello-world/blog/learn-nextjs/blog/deploy-nextjs などの URL にマップされます。このようなページは、pages/blog/[slug].js という名前のファイルによって表現できます。

Next.js は、動的ルートを持つページをサポートしています。例えば、pages/posts/[id].js というファイルを作成すると、それは posts/1、posts/2 などでアクセス可能になります。

pages/
├── index.js  →  / (ルート)
├── blog/
│   ├── index.js  →  /blog (ブログのルート)
│   ├── [slug].js  →  /blog/:slug (ブログの各投稿の動的ルート)
│   ├── tag/
│   │   ├── [tag].js  →  /blog/tag/:tag (ブログの各タグの動的ルート)
│   └── page/
│       └── [page].js  →  /blog/page/:page (ブログの各ページの動的ルート)
└── dashboard/
    └── settings/
        └── username.js  →  /dashboard/settings/username (ダッシュボードの設定の中のユーザー名)

このディレクトリ構造により、動的ルートは URL の一部を動的に切り替えて、多数のページを表現することができます。例えば[slug].jsは、ブログ投稿のそれぞれの URL(例:/blog/my-first-post/blog/my-second-post)に対応します。

同様に、[tag].jsは各ブログタグのページ(例:/blog/tag/javascript/blog/tag/css)を表し、[page].jsはブログの各ページネーションページ(例:/blog/page/1/blog/page/2)を表します。

タグやページネーションのページ

タグページやページネーションのルーティングについても、同様にネストされたフォルダ構造を使用して自動的にルーティングを設定することが可能です。

例えば、特定のタグに関連する投稿を一覧表示するためのタグページを作りたい場合、pages/tags/[tag].jsというファイルを作成します。ここで、[tag]は動的な部分で、URL のこの部分に入力された値がそのまま[tag]の部分に入ります。そのため、例えばwww.yoursite.com/tags/technologyのような URL を開くと[tag].jsページが開き、technologyという値が[tag]に渡されます。

ページネーションの場合も同様で、pages/posts/page/[page].jsのようなファイルを作成することで、ページネーションを実現できます。ここでも[page]は動的な部分で、例えばwww.yoursite.com/posts/page/2のような URL を開くと、[page].jsページが開き、2という値が[page]に渡されます。

したがって、Next.js では、タグページやページネーションのルーティングも、フォルダとファイルの構造によって簡単に設定することが可能です。

レイアウトパターン

React のモデルを使うと、ページをたくさんのパーツ(これをコンポーネントと言います)に分けることができます。これらのコンポーネントは、いろいろなページで何度も使われます。例えば、同じメニューバーとフッターがすべてのページにあるとします。

components/layout.tsx

// メニューバーとフッターを使うためにインポートします
import Navbar from './navbar'
import Footer from './footer'

// レイアウトという名前の新しいコンポーネントを作ります
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    // レイアウトはメニューバー、
    // 主要な内容(children)、そしてフッターの3つの部分から成り立ちます
    <>
      <Navbar />
      <main>{children}</main>
      <Footer />
    </>
  )
}

ページを三つの部分に分けました。メニューバー、主要な内容、そしてフッター。これは「レイアウトパターン」と呼ばれ、同じパーツがたくさんのページで使われます。

全てのページで同じレイアウトを使う方法

もし、アプリ全体で一つだけのレイアウトがあるなら、カスタムアプリを作ってそのレイアウトでアプリ全体を包むことができます。ページを切り替えても<Layout />コンポーネントは再利用されるので、コンポーネントの状態(例えば、入力した値)は保持されます。

pages/_app.tsx

// レイアウトコンポーネントをインポートします
import Layout from '../components/layout'

// MyAppという名前のカスタムアプリを作ります
export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    // レイアウトでアプリ全体を包みます。
    // それにより全てのページで同じレイアウトが使われます
    <Layout>
      <Component {...pageProps} />
    </Layout>
  )
}

このコードでは、一つのレイアウトをアプリ全体に適用しています。これにより、全てのページが同じメニューバーとフッターを持つことになり、見た目が統一されます。また、ページを切り替えても、入力した値などの状態は保持されるので、使いやすさも保たれます。

つまり、このコードは Next.js のカスタム App を定義しています。MyApp という名前の関数コンポーネントは全てのページで使用され、共有のレイアウト(この場合は Layout コンポーネント)を適用したり、ページ間での状態を保持したりするために使用されます。MyApp は特定のページのコンポーネント(Component)とそのページの props(pageProps)を受け取り、それらを Layout コンポーネントでラップします。

それぞれのページごとに違うレイアウトを使う方法

複数のレイアウトが必要なら、各ページに「getLayout」なんていう特別なものを追加して、レイアウトとして使いたい部品を返すようにすることができます。これによって、それぞれのページごとに異なるレイアウトを作ることができます。関数を返すので、もし必要なら複雑な入れ子になったレイアウトも作れますよ。

pages/index.tsx

// 必要な部品を取り入れます
import Layout from '../components/layout'
import NestedLayout from '../components/nested-layout'

// Pageという名前のページを作ります
export default function Page() {
  return (
    // ここにページの中身を書きます
  )
}

// getLayoutという特別なものを追加して、レイアウトとして使いたい部品を返すようにします
Page.getLayout = function getLayout(page: ReactElement) {
  return (
    // 入れ子になったレイアウトを作っています
    <Layout>
      <NestedLayout>{page}</NestedLayout>
    </Layout>
  )
}

pages/_app.tsx

// MyAppという名前のカスタムアプリを作ります
export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  // ページごとに定義されているレイアウトを使います、もし存在していれば
  const getLayout = Component.getLayout || ((page) => page)

  return getLayout(<Component {...pageProps} />)
}

ページを移動しても、入力した値やスクロールの位置などの状態(状態とは、ページの現在の状況のことです)を保持(保持とは、状態を覚えておくことです)したいと思うことがよくあります。このレイアウトのやり方を使うと、ページが移動しても部品の木(部品がどのように組み合わされているかを表した図のことです)は維持されるので、React はどの部品が変わったのかを理解して、状態を保持することができます。

TypeScript を使う場合

TypeScript を使う時、最初に「getLayout」という関数を含む新しい型を作らなくちゃいけません。これはページの型です。次に、アプリの設定に関する新しい型を作る必要があります。この新しい型は、さっき作ったページの型を使うように「Component」プロパティを上書きします。

pages/index.tsx

// Reactの要素という型と、さっき作ったページの型を取り入れます
import type { ReactElement } from 'react'
import Layout from '../components/layout'
import NestedLayout from '../components/nested-layout'
import type { NextPageWithLayout } from './_app'

// Pageという名前のページを作ります。このページはさっき作ったページの型に従います
const Page: NextPageWithLayout = () => {
  return <p>hello world</p> // hello worldと表示します
}

// getLayoutという特別なものを追加して、レイアウトとして使いたい部品を返すようにします
Page.getLayout = function getLayout(page: ReactElement) {
  return (
    <Layout>
      <NestedLayout>{page}</NestedLayout>
    </Layout>
  )
}

export default Page // このページを他の場所で使えるようにします

pages/_app.tsx

// 必要な型を取り入れます
import type { ReactElement, ReactNode } from 'react'
import type { NextPage } from 'next'
import type { AppProps } from 'next/app'

// ページの型を定義します。この型にはgetLayoutという特別なものが含まれています
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
  getLayout?: (page: ReactElement) => ReactNode
}

// アプリの設定に関する新しい型を作ります。
// この型はComponentプロパティを上書きして、さっき作ったページの型を使います
type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout
}

// MyAppという名前のカスタムアプリを作ります
export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  // ページごとに定義されているレイアウトを使います、もし存在していれば
  const getLayout = Component.getLayout ?? ((page) => page)

  return getLayout(<Component {...pageProps} />)
}

データフェッチング

レイアウト内で、useEffect を使ったクライアントサイドのデータフェッチングや、SWR のようなライブラリを使用することができます。このファイルはページではないため、現時点では getStaticProps や getServerSideProps は使用できません。

components/layout.tsx

// 必要なモジュールをインポートします
import useSWR from 'swr'
import Navbar from './navbar'
import Footer from './footer'

// Layoutコンポーネントを定義します
export default function Layout({ children }: { children: React.ReactNode }) {
  // useSWRフックを使用して'/api/navigation'からデータをフェッチします。
  // fetcherはデータ取得のための関数です。
  const { data, error } = useSWR('/api/navigation', fetcher)

  // エラーハンドリング: データの取得に失敗した場合、エラーメッセージを表示します
  if (error) return <div>読み込みに失敗しました</div>
  // データがまだ読み込まれていない場合、ローディングメッセージを表示します
  if (!data) return <div>読み込み中...</div>

  // データが読み込まれたら、NavbarとFooterを表示し、その間に子要素を配置します。
  // Navbarにはdata.links(フェッチしたデータ)を渡します。
  return (
    <>
      <Navbar links={data.links} />
      <main>{children}</main>
      <Footer />
    </>
  )
}

上記のコードは TypeScript で書かれた Next.js のレイアウトコンポーネントの例です。このコンポーネントは useSWR フックを使ってサーバーからデータをフェッチし、そのデータを Navbar コンポーネントに渡しています。このようにレイアウトコンポーネントを使うことで、ページ全体に渡って再利用することの多いコード(この場合、データのフェッチとその表示)を一箇所にまとめることができます。

「ページとレイアウト」の実装方法

それでは基本的なページとレイアウトのソースコードを見てみましょう。

まずは単純なページの例から見ていきます。

// pages/index.tsx

import React from 'react'

type Props = {
  message: string
}

const HomePage = ({ message }: Props) => {
  return <div>{message}</div>
}

export default HomePage

上記のコードは pages/index.tsx

ファイルに配置され、Next.js アプリケーションのホームページとして扱われます。このコンポーネントは message というプロパティを受け取り、それを画面に表示します。

次に、レイアウトの例を見てみましょう。

// components/Layout.tsx

import React from 'react'

type Props = {
  children: React.ReactNode
}

const Layout = ({ children }: Props) => {
  return (
    <div>
      <header>Header</header>
      <main>{children}</main>
      <footer>Footer</footer>
    </div>
  )
}

export default Layout

上記の Layout コンポーネントは、ヘッダー、メインコンテンツの部分、フッターを持つ基本的なウェブページの構造を定義しています。children プロパティはページ固有のコンテンツを表し、各ページから渡されます。

Next.js で、ページとレイアウトを実装

Next.js では、ページコンポーネントが URL パスと一対一で対応します。pages ディレクトリに配置されたファイルは自動的にルートとして認識され、そのエクスポートされたデフォルトコンポーネントがページコンポーネントとして扱われます。これにより、独自のルーティング設定を書く必要がなく、ファイルベースで簡単にルーティングを管理できます。

一方、レイアウトはページ間で再利用可能なコンポーネントを定義します。これは主にアプリケーションの共通部分(例えばヘッダーやフッターなど)を管理するために使われます。レイアウトコンポーネントは、ページコンポーネント内でラップする形で使用されます。

では、具体的にページとレイアウトをどのように組み合わせるのか見てみましょう。

// pages/index.tsx

import React from 'react'
import Layout from '../components/Layout'

type Props = {
  message: string
}

const HomePage = ({ message }: Props) => {
  return (
    <Layout>
      <div>{message}</div>
    </Layout>
  )
}

export default HomePage

この HomePage コンポーネントは、先程の Layout コンポーネントをラップとして使用しています。Layout 内にページ固有のコンテンツを配置することで、ページのコンテンツはレイアウトの children として表示されます。

Emotion と組み合わせる

CSS-in-JS ライブラリの一つである

Emotion を用いることで、Next.js のページとレイアウトにスタイルを適用することができます。Emotion を使ってみましょう。

// components/Layout.tsx

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import React from 'react'

type Props = {
  children: React.ReactNode
}

const Layout = ({ children }: Props) => {
  return (
    <div css={styles.container}>
      <header css={styles.header}>Header</header>
      <main css={styles.main}>{children}</main>
      <footer css={styles.footer}>Footer</footer>
    </div>
  )
}

const styles = {
  container: css`
    display: flex;
    flex-direction: column;
    min-height: 100vh;
  `,
  header: css`
    background-color: #f5f5f5;
    padding: 20px;
    text-align: center;
  `,
  main: css`
    flex: 1;
    padding: 20px;
    text-align: center;
  `,
  footer: css`
    background-color: #f5f5f5;
    padding: 20px;
    text-align: center;
  `,
}

export default Layout

ここでは、Layout コンポーネントにスタイルを適用しています。css 関数を使ってスタイルを定義し、css プロパティを使ってスタイルを適用します。

動的なページ作成

Next.js は静的なページだけでなく、動的なページも作成できます。動的なページはブラケット([])で囲まれたファイル名を持ちます。例えば、pages/posts/[id].tsx のようになります。

// pages/posts/[id].tsx

import { useRouter } from 'next/router'
import React from 'react'
import Layout from '../../components/Layout'

const PostPage = () => {
  const router = useRouter()
  const { id } = router.query

  return (
    <Layout>
      <div>Post: {id}</div>
    </Layout>
  )
}

export default PostPage

この PostPage コンポーネントは、動的な URL パス(例えば /posts/1/posts/2 など)を処理します。URL の動的部分は router.query を通じてアクセスできます。これにより、動的にページを生成することが可能となります。

以上が Next.js のページとレイアウトの考え方についての解説です。この考え方を理解し、Next.js と TypeScript を組み合わせて使うことで、効率的にウェブアプリケーションを構築できるようになるでしょう。

Next.js と TypeScript で、Pages と Layout を学ぶ