Type something to search...
Next.js useSelectedLayoutSegments

Next.js useSelectedLayoutSegments

Breadcrumbs look like a solved problem until you actually try to build them inside a nested layout system. You can't just split window.location.pathname on slashes and call it a day — Next.js route groups don't correspond to URL segments, catch-all routes swallow multiple path pieces into one logical segment, and a layout has no built-in way to know what's rendering below it. useSelectedLayoutSegments exists specifically to close that gap: it hands a layout the list of active route segments beneath it, in a form that already accounts for all three of those complications.

It's easy to confuse this hook with its singular sibling, useSelectedLayoutSegment. The singular version returns one string — the immediate child segment one level down, or null if there isn't one. useSelectedLayoutSegments returns an array — every active segment from that point all the way down the tree. If you're building a single active-tab indicator in a top-level nav, you want the singular hook. If you're building a breadcrumb trail, a "you are here" path indicator, or anything else that needs the full depth of the current route, this is the one you want.

What it is and where it runs

useSelectedLayoutSegments is exported from next/navigation, and like every hook in that module it's a Client Component hook — it has to run in the browser because it reads live routing state. Since layouts in the App Router are Server Components by default, you'll almost never call this hook directly inside layout.tsx. The standard pattern is to pull the hook out into a small dedicated Client Component and import that into your layout, keeping the layout itself server-rendered:

// app/dashboard/breadcrumbs.tsx
"use client";

import { useSelectedLayoutSegments } from "next/navigation";

export default function Breadcrumbs() {
  const segments = useSelectedLayoutSegments();

  return (
    <nav aria-label="Breadcrumb">
      <ol className="flex gap-2 text-sm text-gray-500">
        {segments.map((segment, index) => (
          <li key={index} className="capitalize">
            {segment.replace(/-/g, " ")}
          </li>
        ))}
      </ol>
    </nav>
  );
}
// app/dashboard/layout.tsx
import Breadcrumbs from "./breadcrumbs";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <Breadcrumbs />
      {children}
    </div>
  );
}

That split matters for more than just style points. Keeping the layout itself as a Server Component means the surrounding chrome — sidebar, header, whatever else lives in that layout — still renders on the server and streams down without needing to ship its own JavaScript. Only the small breadcrumb component pays the client-side cost.

Signature and parameters

const segments = useSelectedLayoutSegments(parallelRouteKey?: string)

The hook takes one optional argument: a parallelRouteKey. You only need this if the layout you're calling from has parallel routes — named @slot folders that render more than one page in the same layout simultaneously. In that case, each slot has its own independent segment history, and parallelRouteKey tells the hook which slot's segments you want. If your layout is a normal, single-child layout, you can ignore this parameter entirely and call the hook with no arguments — it defaults to the main children slot.

What it returns

The return value is an array of strings, representing every active segment starting one level below the layout that called the hook, all the way down to whatever is currently rendered. If nothing is rendered below (you're already at the deepest matched route), you get an empty array.

The docs lay out the mapping cleanly with a table, and it's worth internalizing because it's the part people get wrong most often — the array is always relative to where you call the hook from, not relative to the root of the app:

LayoutVisited URLReturned Segments
app/layout.js/[]
app/layout.js/dashboard['dashboard']
app/layout.js/dashboard/settings['dashboard', 'settings']
app/dashboard/layout.js/dashboard[]
app/dashboard/layout.js/dashboard/settings['settings']

That last row is the one that trips people up. If you call useSelectedLayoutSegments from app/dashboard/layout.js, you don't get ['dashboard', 'settings'] — you get ['settings'], because dashboard is the layout's own segment, not something below it. Moving the hook to a deeper layout shortens the returned array from the front, not the back.

Route groups and the bracket problem

Route groups — folders named (marketing) or (auth) that let you organize routes or apply a layout to a subset of pages without adding a URL segment — still show up in the returned array, even though they're invisible in the actual URL. If you render the raw array straight into a breadcrumb list, you'll end up with a stray (marketing) or (dashboard) item sitting between real breadcrumb entries, which looks like a bug to anyone who didn't write the routing.

The fix is a one-line filter, and it's worth baking into any reusable breadcrumb component from the start rather than discovering it in a bug report:

"use client";

import { useSelectedLayoutSegments } from "next/navigation";

export default function Breadcrumbs() {
  const segments = useSelectedLayoutSegments().filter(
    (segment) => !segment.startsWith("("),
  );

  return (
    <ol>
      {segments.map((segment, index) => (
        <li key={index}>{segment}</li>
      ))}
    </ol>
  );
}

Catch-all routes collapse into one entry

The other behavior worth internalizing before you build anything with this hook: catch-all segments ([...slug]) don't expand into one array item per URL piece — they collapse into a single joined string. Given app/blog/[...slug]/page.js and a visit to /blog/a/b/c, calling the hook from app/layout.js returns ['blog', 'a/b/c'], not ['blog', 'a', 'b', 'c'].

If you're building a breadcrumb trail and want each path piece as its own clickable link, you need to split that joined string back apart yourself:

"use client";

import { useSelectedLayoutSegments } from "next/navigation";

export default function Breadcrumbs() {
  const rawSegments = useSelectedLayoutSegments();

  const segments = rawSegments
    .filter((segment) => !segment.startsWith("("))
    .flatMap((segment) => segment.split("/"));

  return (
    <ol>
      {segments.map((segment, index) => (
        <li key={index}>{segment}</li>
      ))}
    </ol>
  );
}

This is easy to miss because it only shows up once you test a route with a catch-all segment in it — a plain nested-folder route never exercises this path, so the bug can sit dormant until someone visits a blog post with a slash in its slug.

Cache Components and the Suspense requirement

If your project has the cacheComponents flag enabled, there's a subtlety worth planning around: useSelectedLayoutSegments reads the currently active route, which is exactly the kind of request-time information that can force a component to suspend during prerendering.

Whether it actually suspends depends on whether the segments below the layout are knowable at build time:

  • Static routes, and dynamic routes fully covered by generateStaticParams: every param is known ahead of time, so the active segments can be resolved during prerendering. No Suspense boundary is needed.
  • Dynamic routes with params not covered by generateStaticParams: the param is a fallback that isn't known until someone actually requests it. The hook can't resolve the segments during prerendering, so it suspends — and if nothing above it is wrapped in Suspense, the build fails outright.

The part that catches people off guard: this applies even to breadcrumb components that are themselves completely static. A breadcrumb rendered in a top-level layout will suspend on any page below it that has an unresolved dynamic param, dragging the whole layout's build down with it unless you isolate the breadcrumb behind its own Suspense boundary:

import { Suspense } from "react";
import Breadcrumbs from "./breadcrumbs";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <Suspense fallback={<div className="h-5" />}>
        <Breadcrumbs />
      </Suspense>
      {children}
    </div>
  );
}

Wrapping just the breadcrumb component — not the whole layout — keeps the rest of the page prerenderable while the breadcrumb resolves per-request.

Common mistakes

Rendering route-group brackets straight into the UI. Covered above, but it's the single most common visible bug with this hook — always filter segments starting with ( before rendering.

Assuming the array is relative to the app root. It's relative to the layout you called the hook from. Moving the same breadcrumb component to a deeper layout changes what it returns, even though the code didn't change.

Forgetting catch-all routes collapse. If your breadcrumb UI needs one clickable link per URL piece, you have to split catch-all segments yourself — the hook won't do it for you.

Calling the hook directly inside layout.tsx. Layouts are Server Components by default; useSelectedLayoutSegments needs a Client Component boundary. Pull it into its own small component and import that into the layout instead of marking the whole layout 'use client'.

Skipping the Suspense boundary under Cache Components. If your project uses cacheComponents and any route below the layout has a fallback dynamic param, an unwrapped breadcrumb component will fail the build, not just the specific request.

useSelectedLayoutSegment vs. useSelectedLayoutSegments

useSelectedLayoutSegmentuseSelectedLayoutSegments
ReturnsA single string, or nullAn array of strings (possibly empty)
DepthOne level below the calling layoutEvery level below the calling layout
Typical useHighlighting the active top-level tabBreadcrumb trails, full path indicators
Route groupsSame bracket-filtering caveat appliesSame bracket-filtering caveat applies
Catch-all routesReturns the joined string as-isSame collapsing behavior, inside the array

Key Takeaways

useSelectedLayoutSegments gives a layout visibility into everything rendering beneath it, which is exactly the missing piece for breadcrumbs, path indicators, and any UI that needs to reflect route depth rather than just the current tab. The three things worth remembering before you reach for it: the array is relative to where you call it from, route groups need filtering out by hand, and catch-all segments arrive pre-joined rather than pre-split. Get those three right up front and the rest of the hook is exactly as simple as it looks.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper