【Next.js ✗ App Router】Routingまとめ 〜前半〜

2024/02/01に公開1

はじめに

今回ですが、Next.jsのApp RouterのRoutingについて基礎から内容をまとめていこうと思います。
Routingを使いこなせば、開発の幅もかなり広がると感じました。

本題に入る前に注釈です。
Next.jsのRoutingはそれないに内容が多いので、本記事は全後半に分けております。
以下、後半の記事ですので、合わせて読むと、より理解が深まると思います。
https://zenn.dev/sc30gsw/articles/67aae793e39d74

それでは本題に入ります。

Routing

Defining Routes

まずはルートの定義ですが、ここは基礎なので、サクッといきます。

Next.jsはフォルダを使用してルートを定義するファイルシステムベースのルーティングを採用しています。
例えば、以下のようなディレクトリ構造の場合を見ていきます。

app/
|-- page.tsx
|-- dashboard/
|   |-- page.tsx
|   |-- settings/
|       |-- page.tsx

この場合、ルーティングとの対応は以下となります。

  • app: /
  • dashboard: /dashboard
  • dashboard/settings: /dashboard/settings

このようにpage.tsxないしpage.jsxpage.jsdefault exportしているディレクトリに基づいてルーティングがなされます。
逆に言えば、appディレクトリ内にcomponentsを作成して各コンポーネントを配置するような設計でも、page.tsxdefault exportされていなければ、ルーティングに影響はないということが言えます。

https://nextjs.org/docs/app/building-your-application/routing/defining-routes

Pages and Layouts

次に、各ルート固有のUIであるPagesと共有レイアウト・テンプレートであるLayoutsを作成する方法を解説します。

Next.jsに触れたことがある人はわかると思いますが、Next.jsApp Routerでは特定のファイル名でdefault exportすることで各ページで固有のUIを作成したり、各ページで共通のレイアウトを作成することができます。

Pages

まず、Pagesですが、これはDefining Routesの方でもでてきたpage.tsxのことを指します。
以下の例を用いて解説すると、URLが/のルートの場合はapp/page.tsxでルートに固有のUI(99%トップページですが)を作ることができます。

また、app/dashboard/page.tsxではURLが/dashboardのときのページのUIを作成することになります。

app/
|-- page.tsx
|-- dashboard/
|   |-- page.tsx
|   |-- settings/
|       |-- page.tsx

Layouts

次にLayoutsですが、これはページで共通のUIを作成するために用います。

app/
|-- page.tsx
|-- dashboard/
|   |-- page.tsx
|   |-- layout.tsx
|   |-- settings/
|       |-- page.tsx
|       |-- layout.tsx

上記の場合、URLが/dashboardのときには共通のUIとしてlayout.tsxの内容が表示されることになります。
また、layout.tsx`は一度ルーティングによりナビゲートされると、状態を保持する仕様になっているので再レンダリングされないという特徴があります。

layout.tsxは以下のようにpropsとしてchildrenを受け取ることができるので、page.tsxの内容をchildrenとして表示することができます。

layout.tsx
import React, { ReactNode } from 'react'

const DashboardLayout = ({ children }: Readonly<{ children: ReactNode }>) => {
  return (
    <div>
      <div>DashboardLayout</div>
      <div>{children}</div>
    </div>
  )
}

export default DashboardLayout

上記のルーティングの例では、ルーティングがネストされており、dashboard/以下ではlayoutが2つあります。このようにlayoutがネストされると、子ルート(この場合は、settings配下)では、共通のレイアウトが積み上げられるUIになります。

要するに、DOM上では以下のようになります。
あくまでイメージしやすいように例として示しただけなので、実際の実装とは大きく異なることはご了承ください。

<body>
  <DashboardLayout />
  <SettingsLayout>
    <SettingsPage /> ← layout.tsxに渡されるprops.children
  </SettingsLayout>
</body>

画面にすると以下のようになります。
layout

Template

Next.jsにはもう一つ共通UIを作成するものがあります。
それがTempalteというものです。

作成方法はlyaoutと同じでtemplate.tsxを作成するだけです。

app/
|-- page.tsx
|-- dashboard/
|   |-- page.tsx
|   |-- template.tsx
|   |-- settings/
|       |-- page.tsx
|       |-- template.tsx

ただし、layoutとの大きく違う点があります。
先程、layoutでは状態が保持され、再レンダリングがされないと解説しましたが、templateはその逆で、状態が保持されず、ルートにナビゲートするたびにコンポーネントが再レンダリングされます。

つまり、ページにナビゲートされるたびに、必ず実行したい機能があるなどのケースで有用ということです。
(例えば、必ず実行したいアニメーションがあるなど)

細かくは言及しないので、以下の記事を読むことをおすすめしますが、とりあえず状態の保持に違いがあるということを認識しておいてください。
https://zenn.dev/cybozu_frontend/articles/8caf1decb1e82c

headの機能ではページごとにメタデータを定義することができるというものです。
ReactなどSPANではSEO対策が不十分という弱点があるのでNext.jsでは、このような弱点にも対応できるという機能です。

https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts

Linking and Navigating

Next.jsではルート間を移動する方法が4つあります。

  • Linkコンポーネント
  • useRouterフック(クライアントコンポーネントでのみ使用可能)
  • redirect(サーバーコンポーネントでのみ使用可能)
  • History API

Next.jsではLinkを推奨しているので、プログラム的(フォーム送信後に特定の画面に遷移するなど)にルーティングを設定したい場合を除いて、Linkを使用するようにしましょう。

Linkコンポーネント

これは以下のように使用します。
aタグと同じように使用します。(実際Linkコンポーネントはaタグでレンダリングされます)

動的ルートの場合は、hrefに{/blog/${post.id}}などを渡して生成します。

import Link from 'next/link'
 
export default function Page() {
  return <Link href="/dashboard">Dashboard</Link>
}

aタグでできることは基本できるので、#sectionなどもaタグと同じ挙動となります。

また、Linkコンポーネントはプリフェッチされるため、リンク先のページを事前に読み込んでおくことができます。(JavaScriptなどのリソースをダウンロードするなどを行う)
そのため、レンダリングコストが下がるため、高速に画面遷移することができます。

このLinkのプリフェッチは静的ルートと動的ルートで違いがあります。

  • 静的ルート: ルート全体がプリフェッチされ、キャッシュされる
  • 動的ルート: loading.tsxまでレンダリングされたコンポーネントのツリーの下にある共通レイアウトのみがプリフェッチされキャッシュされる

このようなことが行われるので、Next.jsはLinkを推奨しているともいえます。

ちなみに、ブラウザではページ間を移動する度に、ハードナビゲーションが実行されるので、ページの全てのUIがレンダリングされます。それに比べてNext.jsはソフトナビゲーションを実行します。
要するにReactの仮想DOMの技術と同じで更新があるUIのみ更新します。
つまり、ページ間の移動でもソフトナビゲーションが実行されるため、高速のページ移動が可能となるということです。
豆知識的なことですが、「だから、速いのか」と合点がいくと思います。

useRouter

クライアントコンポーネントでのみ使用します。
以下の例では、pushで遷移するようになっています。

'use client'
 
import { useRouter } from 'next/navigation'
 
export default function Page() {
  const router = useRouter()
 
  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  )
}

redirect

サーバーコンポーネントでのみ使用します。
この場合は、teamが取得できなかった場合にログイン画面にリダイレクトされるようになっています。

ちなみに、redirectは絶対URLも渡せるので外部リンクにリダイレクトさせることもできます。

import { redirect } from 'next/navigation'
 
async function fetchTeam(id: string) {
  const res = await fetch('https://...')
  if (!res.ok) return undefined
  return res.json()
}
 
export default async function Profile({ params }: : { params: { id: string }) {
  const team = await fetchTeam(params.id)
  if (!team) {
    redirect('/login')
  }
 
  // ...
}

History API

Next.jsではブラウザの履歴スタックによるルーティングを設定することもできます。
以下の例では、URLのクエリパラメータに?sort=ascなどのパラメータを設定し、ブラウザの履歴に新しく追加しています。

これにより、ユーザーはブラウザバックした際にもURLのクエリパラメータを保持するため、ソートされた結果を表示するなどが行なえます。

'use client'
 
import { useSearchParams } from 'next/navigation'
 
export default function SortProducts() {
  const searchParams = useSearchParams()
 
  function updateSorting(sortOrder: string) {
    const params = new URLSearchParams(searchParams.toString())
    params.set('sort', sortOrder)
    window.history.pushState(null, '', `?${params.toString()}`)
  }
 
  return (
    <>
      <button onClick={() => updateSorting('asc')}>Sort Ascending</button>
      <button onClick={() => updateSorting('desc')}>Sort Descending</button>
    </>
  )
}

以下の例は、ブラウザの履歴スタックを書き換える例です。
例えば、Englishを押した場合、/locale/enに遷移します。
その際に、元いたURLを/locale/enに置き換えます。
(元いたパスが/settings/localeなら、そのパスを置き換えます)
つまり、ユーザーはブラウザバックしても元の画面に戻ることができません。

使い所は限られますが、以下のようにlocaleを切り替えるなどの場合に使用されるようです。

'use client'
 
import { usePathname } from 'next/navigation'
 
export function LocaleSwitcher() {
  const pathname = usePathname()
 
  function switchLocale(locale: string) {
    const newPath = `${pathname}/${locale}`
    window.history.replaceState(null, '', newPath)
  }
 
  return (
    <>
      <button onClick={() => switchLocale('en')}>English</button>
      <button onClick={() => switchLocale('fr')}>French</button>
    </>
  )
}

https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating

Loading UI and Streaming

Next.jsではReact Suspenseとしてページ全体をローディングにすることができる機能があります。これも実装自体は簡単でlayout.tsxと同じです。
以下のように、layout.tsxを作成し、default exportします。
このファイルの内容をSpinnerにすればローディングUIが完成します。

app/
|-- page.tsx
|-- dashboard/
|   |-- page.tsx
|   |-- loading.tsx
|   |-- settings/
|       |-- page.tsx
|       |-- loading.tsx

サーバーコンポーネントであれば、fetchしてawaitしている間にloading.tsxの内容が表示されるという機能になっています。

また、ReactのSuspense fallbackも使用してストリーミングHTMLで、レンダリングもしくはローディングが完了した部分から順に表示していくUIも作成できます。

実装方法はReactと同じで、<Suspense fallback={<LoadingUI />}></Suspense>として、ローディングにしたい、コンポーネントをSuspense内に指定するだけでストリーミングHTMLが実装できます。

https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming

Error Handling

次にエラーハンドリング(Error Boundary)ですが、Next.jsではエラーページも簡単に実装できます。
これも例のごとくerror.tsxを作成するだけで実装できます。
Next.jsではこれだけで、page.tsxとその子コンポーネントをError Boundaryでラップしてくれます。

エラーで影響が受けるのは、そのページ内に留めることができるのも良い点だと思います。

app/
|-- page.tsx
|-- dashboard/
|   |-- page.tsx
|   |-- error.tsx
|   |-- settings/
|       |-- page.tsx
|       |-- error.tsx

error.tsxは以下のようにクライアントコンポーネントで実装する必要があります。
また、reset()関数を使用することで、エラーからの復帰を図ることもできます。
例えば、fetchに失敗してerror.tsxがレンダリングされた際は再度、データをfetchしにいくことができます。

また、エラーの情報もpropsで受け取ることができるので、ログを残すこともできます。

'use client' // Error components must be Client Components
 
import { useEffect } from 'react'
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error)
  }, [error])
 
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button
        onClick={
          // Attempt to recover by trying to re-render the segment
          () => reset()
        }
      >
        Try again
      </button>
    </div>
  )
}

以下に示したようにerror.tsxがネストされている場合は、エラーが起きたときに最も近いerror.tsxが表示されます。
例えば、settings/page.tsxでエラーが発生した場合は、settingsのerror.tsxが表示されます。

app/
|-- page.tsx
|-- dashboard/
|   |-- page.tsx
|   |-- error.tsx
|   |-- settings/
|       |-- page.tsx
|       |-- error.tsx

また、以下のケースで、settings/page.tsxでエラーが起きた場合は、dashboardのerror.tsxが表示されます。
要するにエラーハンドリングを親か子で指定することも簡単にできたり、親にバブルアップすることを制御できるなど、柔軟なエラーハンドリングができるということです。

app/
|-- page.tsx
|-- dashboard/
|   |-- page.tsx
|   |-- error.tsx
|   |-- settings/
|       |-- page.tsx

https://nextjs.org/docs/app/building-your-application/routing/error-handling

Dynamic Routes

Dynamic Routesは、いわゆる動的ルーティングをファイルシステムベースのルーティングで行う機能です。
Next.jsでは、動的ルートを作成する場合、ビルド時にレンダリング(SSG)されるかリクエスト時にレンダリング(SSR)されるかを設定することができます。

実装方法ですが、フォルダ名を[]で囲むことで作成できます。
以下の場合は、/blog/foo/blog/bar123/blog/1などのルーティングを実現しています。
また[]で囲んだ値をpropsでパラメータとして受け取ることができます。

app/
|-- page.tsx
|-- blog/
|   |-- page.tsx
|   |-- [slug]/
|       |-- page.tsx

以下のようにslugをパラメータとして受け取ることができます。

app/blog/[slug]/page.tsx
export default function Page({ params }: { params: { slug: string } }) {
  return <div>My Post: {params.slug}</div>
}

generateStaticParams

この関数を[slug]/page.tsxでexportすることでビルド時に静的ルートとして生成することができます。(プリフェッチされる)
以下のように使用します。

export async function generateStaticParams() {
  const posts = await fetch('https://.../posts').then((res) => res.json())
 
  return posts.map((post) => ({
    slug: post.slug,
  }))
}

このようにデータフェッチすることで、リクエストが自動でメモ化されます。
これにより、https://.../postsのリクエストが1回のみ行われることを保証するので、ビルド時間の短縮につながります。

Catch-all Segments

Dynamic Routesではカッコ内に省略記号を用いることもできます。
それにより、後続のルートをすべてキャッチすることができます。

例えば、app/shop/[...slug]/page.tsx/shop/cloths/shop/clothes/tops/shop/clothes/tops/t-shirtsと一致するルーティングとなります。
各ルートのパラメータは以下のようになります。

  • /shop/cloths: { slug: ['clohts']}
  • /shop/clothes/tops: { slug: ['cloths', 'tops']}
  • /shop/clothes/tops/t-shirts: { slug: ['clothes', 'tops', 't-shirts']}

Optional Catch-all Segments

さらに、以下のように二重括弧で囲むこともできます。

  • app/shop/[[...slug]]/page.tsx

これは、/shopとも/shop/clothsとも/shop/clothes/tops/shop/clothes/tops/t-shirtsとも一致させることができます。

Catch-all Segmentsとの違いは、パラメータのないルートも一致するという点に違いがあります。
取得できるパラメータは以下のようになり、パラメータのないルートはパラメータも空のオブジェクトになります。

  • /shop: {}
  • /shop/cloths: { slug: ['clohts']}
  • /shop/clothes/tops: { slug: ['cloths', 'tops']}
  • /shop/clothes/tops/t-shirts: { slug: ['clothes', 'tops', 't-shirts']}

https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes

おわりに

前半は以上となります。
基礎的な内容も多かったと思いますが、読んでいただきありがとうございます。
Next.jsの概念的な部分も学べたのではないかと思いますので、ご参考になれば幸いです。

後半は以下から読むことができます。
https://zenn.dev/sc30gsw/articles/67aae793e39d74

参考文献

https://nextjs.org/docs/app/building-your-application/routing/defining-routes
https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts
https://zenn.dev/cybozu_frontend/articles/8caf1decb1e82c
https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating
https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming
https://nextjs.org/docs/app/building-your-application/routing/error-handling
https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes
https://zenn.dev/sc30gsw/articles/67aae793e39d74

Discussion