【Next.js ✗ App Router】Routingまとめ 〜後半〜
はじめに
Next.jsのApp Router
のRoutingのまとめの後半になります。
前半はNext.js触ったことがある人なら大体分かる内容で、基礎的な部分が多かったと思います。
今回は、前回と比べると若干、難解な部分があるとは思いますが、これを理解すれば、実装の幅はかなり高まると思うので、やっていきましょう。
前半の記事も読みたい方は以下から読むことができます。
Routing
Route Groups
これは、appディレクトリ内のディレクトリに対して使用できる機能です。
通常、appディレクトリではネストされたフォルダーはURLパスにマッピングされます。
ただ、Route Groups
を使えば、該当のフォルダーがURLパスにマッピングされないようにすることができます。
これにより、同じルートレベルで共通のレイアウトを使用することができたりします。
実装は以下のようにグループ化したいディレクトリを()
で囲むことで実装できます。
app/
|-- page.tsx
|-- (dashboard)
| |-- layout.tsx
| |-- admin/
| |-- page.tsx
| |-- users/
| |-- page.tsx
この実装では、/admin
・/users
でアクセスします。
(/dashboard
は含まれません)
このようにすることで、ネストされたadminとusersに対して、dashboardのlayout.tsxを共通レイアウトに指定することができます。
もちろん、複数のグループを作成することも可能で、例えば、同じ階層レベルで別のグループを作れば、そのグループのみに適用するレイアウトを作ることができます。
このようにURLに影響は与えることなく、レイアウトの作成ができるため、わざわざURLを増やしてまで実装すべきでない場合や様々な要件に対応することが可能です。
Parallel Routes
これは並列ルートと呼ばれるもので、同じレイアウト内で1つ以上のページを同時にレンダリングしたり、条件付きでレンダリングすることができます。
並列ルートは、以下のようにスロットを使用して@ディレクトリ名
とします。
例えば、以下の場合は、/dashboard
のルートに並列で@analytics
と@team
が定義されている状態です。
(こちらもRoute Groups
と同様にスロットのディレクトリはURLに含まれることはありません)
app/
|-- page.tsx
|-- dashboard/
| |-- layout.tsx
| |-- page.tsx
| |-- @anlytics/
| |-- page.tsx
| |-- @team
| |-- page.tsx
並列ルートを使用するとなにができるのか、ここまでだといまいちピンときていないと思います。
実際の、実装を見ると、すごさがわかります。
import React, { ReactNode } from 'react'
const DashboardLayout = ({
children,
team,
analytics,
}: Readonly<{
children: ReactNode
team: ReactNode
analytics: ReactNode
}>) => {
return (
<div>
<div>DashboardLayout</div>
<div>{team}</div>
<div>{analytics}</div>
<div>{children}</div>
</div>
)
}
export default DashboardLayout
各スロットの実装は以下のとおりです。
import React from 'react'
const DashboardAnalytics = () => {
return <div>DashboardAnalytics</div>
}
export default DashboardAnalytics
import React from 'react'
const DashboardTeam = () => {
return <div>DashboardTeam</div>
}
export default DashboardTeam
上記のように、共通レイアウトの部分で複数のページをレンダリングすることができます。
実際の画面上では以下のようになります。
このように複数のページを1つのレイアウトで共通化することができます。
例えば、X(旧Twitter)
のタイムラインの投稿一覧UIとサイドのトレンドの部分などを並列ルートでレンダリングするという使い方などができると思います。
default.tsx(js・jsx)
この並列レンダリングではdefault.tsx
を定義することで、スロット内や該当のルートにスロットのpage.tsx
がない場合にレンダリングするファイルを定義することができます。
公式では、以下のような定義となっています。
default.js初期ロードまたはページ全体のリロード中に、一致しないスロットのフォールバックとしてレンダリングするファイルを定義できます。
例えば、以下のルーティングがあるとしましょう。
app/
|-- page.tsx
|-- dashboard/
| |-- layout.tsx
| |-- page.tsx
| |-- @anlytics/
| |-- page.tsx
| |-- default.tsx
| |-- @team
| |-- settings/
| | |-- page.tsx
| |-- default.tsx
| |-- page.tsx
| |-- setings/
| |-- page.tsx
そして、dashboard
のlayoutは上記と同様に、以下とします。
import React, { ReactNode } from 'react'
const DashboardLayout = ({
children,
team,
analytics,
}: Readonly<{
children: ReactNode
team: ReactNode
analytics: ReactNode
}>) => {
return (
<div>
<div>DashboardLayout</div>
<div>{team}</div>
<div>{analytics}</div>
<div>{children}</div>
</div>
)
}
export default DashboardLayout
この場合、/dashboard
にアクセスした際は、@analytics
も@team
も各スロット内のpage.tsx
をレンダリングします。
次に/dashboard/settings
にアクセスする場合を見ていきましょう。
この場合は、@team
には@team/settings/page.tsx
があるので、それをレンダリングします。
ところが、@analytics
には@analytics/settings/page.tsx
どころか、settings
ディレクトリすらありません。
しかし、dashboard/layout.tsx
ではしっかりと@analytics
スロットのページがレンダリングされています。
このような場合に、default.tsx
がレンダリングされます。
また、このような場合にdefault.tsx
がないと、Next.jsは404ページを表示します。
まとめると、スロット内でも該当のルートに対応するページが存在しないといけないという前提があるため、ルートでdefault.tsx
を実装する必要があるということです。
ちなみに、上記ルーティングの実際の画面上の表示は以下のようになります。
- DashboardLayout: layoutの一番上のdiv
- Team Settings: @team/settings/page.tsx
- analytics: @analytics/default.tsx
- DashboardLayoutのchildren: dashboard/settings/page.tsx
条件付きレンダリング
並列ルートでは、以下のように条件付きでレンダリングするレイアウトを変更することができます。
import { checkUserRole } from '@/lib/auth'
export default function Layout({
user,
admin,
}: {
user: React.ReactNode
admin: React.ReactNode
}) {
const role = checkUserRole()
return <>{role === 'admin' ? admin : user}</>
}
さらに、並列ルートの応用として、次に紹介するInterceptiong Routes
との組み合わせでモーダルを作成するなどができます。(これは公式ドキュメントでも記載があるので、見てみてください)
Intercepting Routes
これは言葉のままなのですが、ルートを補足することができる機能です。
何ができるのかというと、現在のページから別のルートを読み込むことができます。
これにより、ユーザーはわざわざページ遷移せずとも別のルートに存在するコンテンツを見ることができます。
例えば、SNS投稿一覧から詳細を表示する場合に、詳細をモーダル表示したいという要件があるとします。その場合、この機能を使用すれば、一覧ページから該当の投稿詳細をページ遷移することなく、モーダルなどで表示することができます。
以下は、簡易的な実装例になるのですが、モーダルを表示した際に、URLが/todos
から/todo/view/[id]
になっているのがわかると思います。
これがInterception Routes
です。
また、モーダルを表示したら、そのまま詳細いきたいということもあると思います。
その場合、ページを更新すると詳細ページに遷移するという仕様になっているようです。
もしくは、共有可能なURLをクリックして遷移させるという方法もあります。
では、具体的に、どのように実装したら良いのかという部分に行きたいと思います。
Intercepting Routesは以下のようなルーティングをすることで実装できます。
まず、補足したいルートを決めるのですが、今回は一覧から詳細を補足したいので、todosのルート内で補足することになります。
そのため、todosルート内に(..)todo/
としてtodo
ルートを補足するようルーティングします。
この(..)
などはimport文のfrom句の部分と同じ内容を示します。
公式での区分は以下のようになっています。
- (.)同じレベルのセグメントを一致させる
- (..)1 つ上のレベルのセグメントと一致する
- (..)(..)2 レベル上のセグメントと一致する
- (...)ルート appディレクトリのセグメントと一致するようにする
app/
|-- page.tsx
|-- todo/
| |-- view/
| |-- [id]
| |-- page.tsx
|-- todos/
| |-- layout.tsx
| |-- page.tsx
| |-- (..)todo/
| |-- view/
| |-- [id]
| |-- page.tsx
次に、実際の実装を見ていきましょう。
まず、詳細ページですが、以下のようになっています。
import React from 'react'
export const TodoView = ({ id }: Readonly<{ id: string }>) => {
return (
<div>
<div>TodoView</div>
<div>{id}</div>
</div>
)
}
import { TodoView } from '@/components/todo/TodoView'
const TodoPage = ({ params }: Readonly<{ params: { id: string } }>) => {
return <TodoView id={params.id} />
}
export default TodoPage
これを補足すると以下のようになります。
import React from 'react'
import { TodoView } from '@/components/todo/TodoView'
import { BackModal } from '@/components/ui/BackModal'
const TodosViewPage = ({ params: { id } }: { params: { id: string } }) => {
return (
<BackModal>
<TodoView id={id} />
</BackModal>
)
}
export default TodosViewPage
このようにすることで、/todos
のURLでtodo詳細を補足し、表示することができます。
ただ、このままだと、ルーティングを補足しただけで、実際に詳細画面をモーダル表示できません。
以下を見るとわかると思います。URLに注目してください。
まずは、一覧画面です。
ここからクリックで詳細をモーダル表示します。
URLが切り替わっていて、一覧が表示されています。
どうやらルートの補足はできているようですが、詳細が表示できていないということがわかります。
この問題を解決するためにParallel Routes
を使用します。
Parallel Routesと組み合わせる
まず、ルーティングを編集します。
@modal
をルーティングに追加するよう修正します。
app/
|-- page.tsx
|-- todo/
| |-- view/
| |-- [id]
| |-- page.tsx
|-- todos/
| |-- layout.tsx
| |-- page.tsx
| |-- @modal/
| |-- default.tsx
| |-- (..)todo/
| |-- view/
| |-- [id]
| |-- page.tsx
そして、layout
を実装します。
import React, { ReactNode } from 'react'
const Layout = ({
children,
modal,
}: Readonly<{ children: ReactNode; modal: ReactNode }>) => (
<div className={'space-y-5'}>
<div className={'text-3xl'}>ToDo リスト</div>
<div>{children}</div>
{modal}
</div>
)
export default Layout
まずchildren
ですが、ここにはtodos/page.tsx
が入ります。
import { ToDoList } from '@/components/todo/TodoList'
import React from 'react'
const TodoListPage = () => {
return (
<div className="bg-slate-400">
<ToDoList />
</div>
)
}
export default TodoListPage
TodoListは以下のようになっています。
useRouter
で遷移したい際に該当の詳細ページを補足するという処理の流れになります。
'use client'
import { useRouter } from 'next/navigation'
export const todoData = [
{ id: '1', title: '片付け' },
{ id: '2', title: '買い物' },
{ id: '3', title: '振込' },
]
export const ToDoList = () => {
const { push } = useRouter()
return (
<div>
{todoData.map((todo) => (
<ul
key={todo.id}
className={'flex space-x-2 border-b-2 p-2 items-center'}
>
{/* biome-ignore lint/a11y/useKeyWithClickEvents: necessary processing */}
<li
onClick={() => push(`/todo/view/${todo.id}`)}
className={'cursor-pointer flex-1'}
>
{todo.title}
</li>
<button
type="button"
onClick={() => push(`/todo/edit/${todo.id}`)}
className={'w-32 bg-blue-300 rounded-md p-1'}
>
編集
</button>
</ul>
))}
</div>
)
}
そしてlayout.tsx
のmodal
には@modal/(..todo)/view/[id]/page.tsx
が入ります。
このパス(/todo/view/[id])以外のパスの場合は、default.tsx
がmodal
に入ります。
まとめると、補足したパス以外のパスの場合にはdefault.tsxがレンダリングされるということです。
今回の場合は、404を表示したいので、default.tsxはnullを返すようにしています。
さらに、先程、詳細ページに遷移するにはブラウザのリロードが必要といいました。
今回はモーダル表示のみにとどめたいので、詳細ページに遷移したい際に/todos
にリダイレクトさせるようにしています。
import { redirect } from 'next/navigation'
const TodoDetailPage = () => {
return redirect('/todos')
}
export default TodoDetailPage
このようにRouting
を駆使すれば、様々な実装ができます。
ちなみに、この実装は以下の記事を大いに参考にしているので、こちらも読んでみてください。
Middleware
ミドルウェアですが、これはリクエストが完了する前に実行する処理を記述することができます。
認証・認可の処理はここで実装します。
これはプロジェクトのルートディレクトリにmiddleware.ts
を作成し、その中に処理を記述することで実装できます。
実装例ですが、公式のサンプルでは、/about
以下のURLに遷移してきたら、/home
にリダイレクトさせるような処理になっています。
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
export const config = {
matcher: '/about/:path*',
}
middlewareの実行順
middlewareには実行順が有り、リクエストが行われた際にどの順序で処理が行われるかが決まっています。具体的には、以下の順序で実行されていきます。
1. next.config.jsからのヘッダー:カスタムHTTPヘッダーが設定されている場合、これが最初に適用されます。
2. next.config.jsからのリダイレクト:カスタムリダイレクトが設定されている場合、これが次に適用されます。
3. ミドルウェア:ミドルウェアが存在する場合、これが次に実行されます。ミドルウェアは、リクエストとレスポンスを操作するためのカスタムロジックを提供します。これには、リダイレクトや書き換えなどが含まれます。
4. next.config.jsからのbeforeFiles書き換え:beforeFilesステージでのカスタム書き換えが設定されている場合、これが次に適用されます。
5. ファイルシステムのルート:public/、_next/static/、pages/、app/などのファイルシステムベースのルーティングが次に適用されます。
6. afterFiles(next.config.jsからの書き換え):afterFilesステージでのカスタム書き換えが設定されている場合、これが次に適用されます。
7. 動的ルート:/blog/[slug]などの動的ルートが次に適用されます。
8. fallback(next.config.jsからの書き換え):fallbackステージでのカスタム書き換えが設定されている場合、これが最後に適用されます。
matcher
matcher
で特定のパスや複数のパスに一致した場合にmiddleware
関数を実行するようにできます。
以下のように実装できます。
export const config = {
matcher: '/about/:path*',
}
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}
また、正規表現も使用でき、特定のパスを除外して、それ以外のパスにマッチした際という条件を指定することもできます。
以下の例も公式サンプルですが、api
・next/static
・next/image
・favicon.ico
で始まるパスを除外しています。
これに配列の2つ目・3つ目の要素に/auth/:path*
や/login/:path*
などを追記すれば、認証周りのUIにのみ遷移させることもできます。
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
middlewareは他にもCookie
を使用できたり、header
の設定ができたりするので、そこは公式ドキュメントを読んでみてください。
(本記事ではそこら辺までは解説しません。)
Redirecting
Next.jsではリダイレクト処理を行う方法がいくつかあります。
以下に示した方法でリダイレクト処理を行うことができます。
API | 目的 | 使用場所 | ステータスコード |
---|---|---|---|
redirect | 突然変異またはイベントの後にユーザーをリダイレクトする | サーバーコンポーネント、サーバーアクション、ルートハンドラー | 307 (一時) または 303 (サーバーアクション) |
permanentRedirect | 突然変異またはイベントの後にユーザーをリダイレクトする | サーバーコンポーネント、サーバーアクション、ルートハンドラー | 308(常設) |
useRouter | クライアント側のナビゲーションを実行する | クライアントコンポーネントのイベントハンドラー | 該当なし |
redirects in next.config.js | パスに基づいて受信リクエストをリダイレクトする | next.config.js | 307 (臨時) または 308 (常設) |
NextResponse.redirect | 条件に基づいて受信リクエストをリダイレクトする | ミドルウェア | どれでも |
それでは、各APIについて見ていきましょう。
redirect
redirect
はサーバーコンポーネント・APIルートハンドラー・サーバーアクションで呼び出すことができます。
以下のように実装します。
'use server'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
export async function createPost(id: string) {
try {
// Call database
} catch (error) {
// Handle errors
}
revalidatePath('/posts')
redirect(`/post/${id}`)
}
revalidatePath
で/posts
に関連するキャッシュを削除し、詳細画面にリダイレクトするようになっています。
redirect
は内部的にエラーがスローされるのでtry-catch
の外で呼び出す必要があるので、そこは注意してください。また、前半の記事でも触れましたが、絶対URLを指定することもできるので、外部リンクにリダイレクトすることもできます。
permanentRedirect
permanentRedierct
を使用するとユーザーを別のURLに永続的にリダイレクトさせることができます。
要するに、ブラウザバックしても元のURLに戻ることはできないとうことです。
この関数も、サーバーコンポーネント・APIルートハンドラー・サーバーアクションで呼び出すことができます。
以下は公式の実装例です。
'use server'
import { permanentRedirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export async function updateUsername(username: string, formData: FormData) {
try {
// Call database
} catch (error) {
// Handle errors
}
revalidateTag('username')
permanentRedirect(`/profile/${username}`)
}
revalidateTag
でユーザー名を更新したあとに、username
タグが指定されているデータフェッチのキャッシュを削除し、キャッシュを更新して、該当のURLにリダイレクトさせます。
この例では更新前のプロフィールの画面に戻れないように永続的なリダイレクト処理が実装されています。
このpermanenteRedirect
も絶対URLを受け入れるので外部リンクを指定することができます。
useRouter push
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>
)
}
ただ、プログラム的にユーザーをリダイレクトさせる必要がない場合は、Next.jsはLink
コンポーネントの仕様を推奨しているので、そこは覚えておきましょう。
redirects in next.config.js
これはnext.config
のredirects
オプションを使用することで実現できます。
このリダイレクト処理は、リクエストのパスを別のパスにリダイレクトするため、ページのURL構造を変更する場合や、事前にわかっているリダイレクトのリストがある場合に便利です。
簡単に言うと、ウェブサイトでURLを変更(Next.jsならディレクトリ構造を変更)した場合だったり、特定の古いURLを新しいURLに紐づけたい場合に有効ということです。
以下のように、各プロパティを指定します。
- source: リダイレクトのもととなるURL
- destination: リダイレクト先のURL
- permanent: リダイレクトが永続的かどうか(trueで永続化)
module.exports = {
async redirects() {
return [
// Basic redirect
{
source: '/about',
destination: '/',
permanent: true,
},
// Wildcard path matching
{
source: '/blog/:slug',
destination: '/news/:slug',
permanent: true,
},
]
},
}
ちなみに、このリダイレクト処理はmiddlewareの前に実行されます。
NextResponse.redirect in middleware
このリダイレクトはリクエストが完了する前に実行されます。
このNextResponse.redirect
は条件(認証・セッションの有無など)に基づいてユーザーをリダイレクトさせる場合だったり、多数のリダイレクト(SNSなど多数のユーザーが使用する場合)に非常に有効です。
(アプリケーションを作るとなると、ほぼ確実に実装することになります)
いかが実装例ですが、ユーザーが認証されていない場合、/login
にリダイレクトする処理になっています。
import { NextResponse, NextRequest } from 'next/server'
import { authenticate } from 'auth-provider'
export function middleware(request: NextRequest) {
const isAuthenticated = authenticate(request)
// If the user is authenticated, continue as normal
if (isAuthenticated) {
return NextResponse.next()
}
// Redirect to login page if not authenticated
return NextResponse.redirect(new URL('/login', request.url))
}
export const config = {
matcher: '/dashboard/:path*',
}
各リダイレクトを状況に応じて使えるようにしておくと良いと思います。
おわりに
今回は、Routingの機能の一つにAPIを実装できるRoute Handlers
については触れていません。
理由としては、フロントエンドのRoutingのほうが実装の幅を広げるための知見としては、マストだと考えたためです。
ただ、APIもNext.jsらしい機能になっているので、興味のある方は読んでみてください。
そして、最終的にはドキュメントを読み込むことが一番だと私自身は思っているので本記事の内容を踏まえた上でドキュメントを読んでみることをおすすめいたします。
少しでも本記事が参考になれば幸いです。
最後になりますが、前半の記事は以下から読むことができます。
参考文献
Discussion