【Next.js ✗ App Router】Routingまとめ 〜前半〜
はじめに
今回ですが、Next.jsのApp RouterのRoutingについて基礎から内容をまとめていこうと思います。
Routingを使いこなせば、開発の幅もかなり広がると感じました。
本題に入る前に注釈です。
Next.jsのRoutingはそれないに内容が多いので、本記事は全後半に分けております。
以下、後半の記事ですので、合わせて読むと、より理解が深まると思います。
それでは本題に入ります。
Routing
Defining Routes
まずはルートの定義ですが、ここは基礎なので、サクッといきます。
Next.jsはフォルダを使用してルートを定義するファイルシステムベースのルーティングを採用しています。
例えば、以下のようなディレクトリ構造の場合を見ていきます。
app/
|-- page.tsx
|-- dashboard/
| |-- page.tsx
| |-- settings/
| |-- page.tsx
この場合、ルーティングとの対応は以下となります。
- app: /
- dashboard: /dashboard
- dashboard/settings: /dashboard/settings
このようにpage.tsxないしpage.jsx・page.jsをdefault exportしているディレクトリに基づいてルーティングがなされます。
逆に言えば、appディレクトリ内にcomponentsを作成して各コンポーネントを配置するような設計でも、page.tsxがdefault exportされていなければ、ルーティングに影響はないということが言えます。
Pages and Layouts
次に、各ルート固有のUIであるPagesと共有レイアウト・テンプレートであるLayoutsを作成する方法を解説します。
Next.jsに触れたことがある人はわかると思いますが、Next.jsのApp 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として表示することができます。
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>
画面にすると以下のようになります。

Template
Next.jsにはもう一つ共通UIを作成するものがあります。
それがTempalteというものです。
作成方法はlyaoutと同じでtemplate.tsxを作成するだけです。
app/
|-- page.tsx
|-- dashboard/
| |-- page.tsx
| |-- template.tsx
| |-- settings/
| |-- page.tsx
| |-- template.tsx
ただし、layoutとの大きく違う点があります。
先程、layoutでは状態が保持され、再レンダリングがされないと解説しましたが、templateはその逆で、状態が保持されず、ルートにナビゲートするたびにコンポーネントが再レンダリングされます。
つまり、ページにナビゲートされるたびに、必ず実行したい機能があるなどのケースで有用ということです。
(例えば、必ず実行したいアニメーションがあるなど)
細かくは言及しないので、以下の記事を読むことをおすすめしますが、とりあえず状態の保持に違いがあるということを認識しておいてください。
head
headの機能ではページごとにメタデータを定義することができるというものです。
ReactなどSPANではSEO対策が不十分という弱点があるのでNext.jsでは、このような弱点にも対応できるという機能です。
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>
</>
)
}
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が実装できます。
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
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をパラメータとして受け取ることができます。
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']}
おわりに
前半は以上となります。
基礎的な内容も多かったと思いますが、読んでいただきありがとうございます。
Next.jsの概念的な部分も学べたのではないかと思いますので、ご参考になれば幸いです。
後半は以下から読むことができます。
参考文献
Discussion
優良な記事をありがとうございます。