App Router でナビゲーションを作っていると、「レイアウト側で、今どのページが開かれているかを知りたい」という場面が出てきます。こういうときに役立つのが useSelectedLayoutSegment と useSelectedLayoutSegments という2つのフックです。この記事では、それぞれが何を返すのか、2つの違い、Client Component でしか使えない点、ナビゲーションのアクティブリンクをハイライトする実用例、usePathname との使い分け、そして Parallel Routes 向けの引数までを、動くコードとあわせて解説します。
目次
レイアウトから「今どのページか」を知りたいとき
App Router では、layout.tsx はその配下のページが切り替わっても再レンダリングされずに残り続けます。サイドバーやタブナビゲーションをレイアウトに置くのはこの性質を活かした典型的な構成ですが、レイアウト自身は「配下でどのページが表示されているか」を素直には知りません。そこで、レイアウトから見て1つ下のアクティブなルートセグメントを教えてくれるのが useSelectedLayoutSegment、配下のアクティブなセグメントをすべて教えてくれるのが useSelectedLayoutSegments です。ここでいうセグメントとは、URL のパスを構成する各区切り(/blog/tech なら blog と tech)にあたる、ルートのひとまとまりのことです。
useSelectedLayoutSegment で1つ下のセグメントを取得する
useSelectedLayoutSegment は、そのフックを呼び出したレイアウトから見て1階層だけ下にあるアクティブなセグメント名を文字列で返します。該当するセグメントが無い場合は null を返します。まずは最小の例で戻り値を確認してみましょう。
'use client'
import { useSelectedLayoutSegment } from 'next/navigation'
export default function SegmentInfo() {
// 1つ下のアクティブなセグメント名。無ければ null が返る
const segment = useSelectedLayoutSegment()
return <p>現在のセグメント: {segment ?? '(なし)'}</p>
}
たとえばこのコンポーネントを app/blog/layout.tsx の中で使い、/blog/tech を表示しているとします。このとき blog レイアウトから見て1つ下のセグメントは tech なので、segment には 'tech' が入ります。/blog/life に移動すれば 'life' に変わります。あくまで「呼び出したレイアウトの1階層下」だけを見ている点がポイントです。
useSelectedLayoutSegments で配下すべてのセグメントを取得する
一方の useSelectedLayoutSegments は、複数形の名前のとおり、そのレイアウトより下にあるアクティブなセグメントを上から順に並べた配列を返します。パンくずリストのように、階層全体を扱いたいときに向いています。
'use client'
import { useSelectedLayoutSegments } from 'next/navigation'
export default function SegmentsInfo() {
// 配下のアクティブなセグメントを配列で受け取る
const segments = useSelectedLayoutSegments()
// 例: /dashboard/settings が表示中なら ['dashboard', 'settings']
return <p>{segments.join(' / ')}</p>
}
このコンポーネントをルート直下の app/layout.tsx で使い、/dashboard/settings を表示していれば、segments は ['dashboard', 'settings'] になります。useSelectedLayoutSegment が「1つ下だけ」を返すのに対し、useSelectedLayoutSegments は「そこから下すべて」を配列で返す、と覚えると違いが整理しやすくなります。
2つのフックの違いを整理する
名前が似ていて混同しやすいので、戻り値と使いどころを表で並べておきます。単数形は1つ、複数形は配列、と対応づけて覚えると迷いにくくなります。
| フック | 戻り値の型 | 返すもの | 向いている用途 |
|---|---|---|---|
useSelectedLayoutSegment() | string | null | 1つ下のアクティブなセグメント名(無ければ null) | タブやリンク単位のアクティブ判定 |
useSelectedLayoutSegments() | string[] | 配下のアクティブなセグメントを並べた配列 | パンくずなど階層全体の扱い |
Client Component 専用である点に注意
この2つのフックは、どちらも Client Component でしか使えません。React のフックである以上、ファイルの先頭に "use client" ディレクティブが必要です。これを書き忘れて Server Component のまま使おうとすると、ビルド時や実行時にエラーになります。
実務では、レイアウト(layout.tsx)そのものは Server Component のまま残し、セグメントを参照するナビゲーション部分だけを "use client" を付けた別コンポーネントに切り出して、レイアウトから読み込む構成がよく使われます。レイアウト全体をクライアント化しなくて済むので、サーバー側で完結できる処理はサーバーに残せます。
ナビゲーションのアクティブリンクをハイライトする
もっとも実用的な使い道が、レイアウト側でナビゲーションの「今いるページ」のリンクを強調表示することです。次の例は app/blog/layout.tsx から読み込む想定のナビゲーションで、useSelectedLayoutSegment の戻り値と各リンクの slug を突き合わせてアクティブかどうかを判定しています。
'use client'
import Link from 'next/link'
import { useSelectedLayoutSegment } from 'next/navigation'
const links = [
{ slug: 'tech', label: '技術' },
{ slug: 'life', label: '暮らし' },
]
export default function BlogNav() {
// app/blog/layout.tsx で使うと、/blog/[slug] のセグメント名が返る
const segment = useSelectedLayoutSegment()
return (
<nav>
{links.map((link) => {
const isActive = link.slug === segment
return (
<Link
key={link.slug}
href={`/blog/${link.slug}`}
aria-current={isActive ? 'page' : undefined}
style={{ fontWeight: isActive ? 'bold' : 'normal' }}
>
{link.label}
</Link>
)
})}
</nav>
)
}
/blog/tech を表示していれば segment は 'tech' なので、tech のリンクだけ isActive が true になり、太字とアクセシビリティ用の aria-current="page" が付きます。ナビゲーションはレイアウトに置かれているため、ページを切り替えても再マウントされず、アクティブ表示だけがスムーズに切り替わります。
usePathname との使い分け
「現在地を知る」という点では usePathname も候補になります。usePathname は /blog/tech のようなパス文字列そのものを返すのに対し、useSelectedLayoutSegment は呼び出したレイアウトからの相対的なセグメント名を返します。この違いが使い分けの基準になります。
レイアウトの階層を基準に「その1つ下がどれか」を知りたい、つまりネストしたナビゲーションのアクティブ判定なら useSelectedLayoutSegment が素直です。リンクごとのフルパスと現在地を厳密に突き合わせたい、あるいはクエリ文字列やパスの一部を細かく判定したいなら usePathname のほうが扱いやすくなります。レイアウトに紐づく相対的な判定は前者、絶対的なパス比較は後者、と考えるとよいでしょう。
parallelRoutesKey で Parallel Routes のスロットを指定する
どちらのフックも、省略可能な引数 parallelRoutesKey を1つ受け取れます。これは Parallel Routes(並列ルート)を使い、@auth のような名前付きスロットを持つレイアウトで、特定のスロット内のアクティブセグメントを取得したいときに指定します。
'use client'
import { useSelectedLayoutSegment } from 'next/navigation'
export default function AuthStatus() {
// @auth スロットのアクティブセグメントを取得する
const authSegment = useSelectedLayoutSegment('auth')
// 例: @auth/login が表示中なら 'login' が返る
return <p>認証スロット: {authSegment ?? '未表示'}</p>
}
スロット名の @ は付けずに、'auth' のようにキー名だけを渡します。引数を省略した場合は、通常のページ(children にあたるスロット)のセグメントが対象になります。Parallel Routes を使っていないうちは、この引数を意識する必要はありません。
セグメントが取得できず null や空配列になるとき
戻り値が期待とずれる場合、多くは「そのレイアウトから見て下にアクティブなセグメントが無い」状態です。挙動を理解しておくと、判定ロジックのつまずきを避けられます。
そのレイアウト直下のページを開いているとき
useSelectedLayoutSegment は、1つ下にアクティブなセグメントが無いとき null を返します。たとえば app/blog/layout.tsx の中でこのフックを呼び、/blog(blog 直下の page.tsx)そのものを表示している場合、さらに下のセグメントは存在しないため null になります。アクティブ判定を slug === segment のように書くとき、segment が null になり得ることを前提に組み立ててください。同様に useSelectedLayoutSegments も、下にセグメントが無ければ空配列を返します。
Route Group が配列に含まれる
useSelectedLayoutSegments が返す配列には、(marketing) のような Route Group(ルートグループ)も含まれることがあります。Route Group は URL には現れませんが、フックの戻り値には現れるため、パンくずなどでそのまま表示すると意図しない項目が混ざります。UI に出す前に、括弧で囲まれたグループ名を除外するなどのフィルタリングを検討してください。
Server Component のまま呼び出している
戻り値以前にエラーで動かない場合は、"use client" の付け忘れが定番の原因です。前述のとおりこれらは Client Component 専用のフックなので、ファイル先頭のディレクティブを確認し、必要ならナビゲーション部分をクライアントコンポーネントとして切り出しましょう。
まとめ
useSelectedLayoutSegment は、呼び出したレイアウトから見て1つ下のアクティブなセグメント名を string | null で返し、useSelectedLayoutSegments は配下のアクティブなセグメントを string[] の配列で返します。単数形はタブやリンクのアクティブ判定に、複数形はパンくずのような階層全体の扱いに向いています。どちらも Client Component 専用なので "use client" が必要で、レイアウト本体はサーバーに残しつつナビゲーション部分だけを切り出す構成が定番です。フルパスで比較したいときは usePathname、レイアウト基準の相対判定なら本記事のフック、と使い分けましょう。省略可能な parallelRoutesKey で Parallel Routes のスロットも指定でき、下にセグメントが無いときは null や空配列が返る点、Route Group が配列に混ざる点を押さえておけば安心です。