【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
優良な記事をありがとうございます。