Type something to search...
Next.js next/root-params

Next.js next/root-params

Every Next.js developer runs into the same annoyance eventually: a route parameter that belongs to the whole application — a locale, a tenant slug, a workspace ID — needs to be read three or four components deep, in a shared utility, or inside a cached function that has no idea what route it's being called from. The usual fix is prop drilling: thread the value down through every layout and component in between, or reach for a React Context provider just to avoid typing props.children fifteen times.

next/root-params exists specifically to remove that friction for one narrow but very common case — dynamic segments that live above your root layout. Instead of passing them down as props, you import a getter function and call it from anywhere on the server, no matter how deep in the tree you are. It's a small module, but it changes how you'd structure internationalized routing, multi-tenant apps, and anything else that hangs a shared identifier off the top of your URL structure.

What Counts as a "Root Parameter"

Not every dynamic segment qualifies. A root parameter is specifically a dynamic segment that appears in the path above your root layout file — the layout that wraps <html> and <body>. If your folder structure looks like this:

app/
  [lang]/
    layout.tsx      ← root layout
    page.tsx
    blog/
      [slug]/
        page.tsx

lang is a root parameter because it sits above layout.tsx, the root layout. slug is not — it's a regular route parameter, scoped to whatever page defines it, and you still read it the normal way, through the params prop.

This distinction matters because it's the whole reason the API is safe to use the way it is. A root parameter is guaranteed to exist (or not exist) consistently for every single route under that root layout — it can't suddenly mean something different three levels down, the way a param named slug might mean a blog post in one branch of your route tree and a product ID in another. Route parameters below the root layout don't have that guarantee, which is exactly why they're still restricted to the params prop of the specific page or layout that declares them, rather than being globally importable.

Basic Usage

Say your root layout lives at app/[lang]/layout.tsx. Import the parameter by name — the export name Next.js generates matches your folder's segment name exactly — and call it as an async function anywhere on the server:

// app/[lang]/layout.tsx
import { lang } from "next/root-params";

export default async function RootLayout(props: LayoutProps<"/[lang]">) {
  return (
    <html lang={await lang()}>
      <body>{props.children}</body>
    </html>
  );
}

That's the entire setup. No provider, no context, no prop signature changes anywhere in the tree. Because it's just a module import, you can call lang() from a deeply nested Server Component, a shared server-side utility function, or a data-fetching helper — anywhere that runs on the server.

// lib/get-translations.ts
import { lang } from "next/root-params";

export async function getTranslations() {
  const language = await lang();
  return import(`@/locales/${language}.json`);
}

Notice there's no import "server-only" guard needed here. next/root-params already fails at build time if it's ever imported into a Client Component, so you get that protection for free.

Working with generateStaticParams

If you're using the older caching model, root parameters are simply available as soon as the route exists — no extra setup required. Under Cache Components, though, generateStaticParams becomes mandatory for each root parameter, since the build needs at least one concrete value to work with:

// app/[lang]/layout.tsx
import { lang } from "next/root-params";

export default async function RootLayout(props: LayoutProps<"/[lang]">) {
  return (
    <html lang={await lang()}>
      <body>{props.children}</body>
    </html>
  );
}

export async function generateStaticParams() {
  return [{ lang: "en" }, { lang: "fr" }];
}

With more than one root parameter, every combination you want prerendered needs its own entry:

export async function generateStaticParams() {
  return [
    { lang: "en", locale: "us" },
    { lang: "en", locale: "uk" },
  ];
}

There's a genuinely nice side benefit here: inside a nested segment's own generateStaticParams, you can call the root parameter getter directly instead of destructuring it out of the params argument that Next.js passes down:

// app/[lang]/posts/[slug]/page.tsx
import { lang } from "next/root-params";

export async function generateStaticParams() {
  const language = await lang();
  const posts = await fetch(
    `https://api.example.com/posts?lang=${language}`,
  ).then((res) => res.json());
  return posts.map((post) => ({ slug: post.slug }));
}

That might look like a minor convenience, but it removes an entire category of bugs where someone forgets to pass a parent's route param through to a child's static-params function.

The Caching Payoff

This is where next/root-params earns its keep, and it's the part the basic examples undersell. Because a root parameter getter is an imported function rather than a value handed to you through props, Next.js can statically see exactly which root parameters a given cached function actually touches — and only those parameters become part of that function's cache key.

// app/[lang]/components/cached-nav.tsx
import { lang } from "next/root-params";

async function getNavigation() {
  "use cache";
  const language = await lang();
  const res = await fetch(`https://api.example.com/nav?lang=${language}`);
  return res.json();
}

Compare that to the alternative, where you're passing the parameter in as an argument:

// Without root params — you must await params outside the cached function
async function getDataWithParams(language: string) {
  "use cache";
  return fetch(`https://api.example.com/data?lang=${language}`);
}

export default async function Page(props: PageProps<"/[lang]">) {
  const { lang: language } = await props.params;
  const data = await getDataWithParams(language);
}

Both approaches end up caching per-language, which is what you want. But the second pattern forces you to resolve params outside the cached function and thread it in as a call argument every single time you want to use it — which is exactly the prop-drilling problem this module was built to avoid, just one level removed (argument-drilling instead of prop-drilling). If your app has a dozen dynamic segments above the root layout and a cached function only cares about one of them, next/root-params keeps your cache key scoped to that one parameter instead of accidentally coupling it to route parameters it never touches.

Multiple Root Layouts

Things get more interesting if your app has more than one root layout — a common setup when, say, your dashboard and your marketing site live in the same project but have completely different top-level layouts:

app/
  dashboard/[id]/layout.tsx    ← root layout, has `id`
  marketing/layout.tsx          ← root layout, no `id`

Since id doesn't exist under the marketing root layout, TypeScript types the getter's return as string | undefined rather than a guaranteed string. Calling await id() from a marketing route resolves to undefined, and your code needs to handle that. This is the one place where the "root parameters are guaranteed" pitch has an asterisk: guaranteed within a given root layout's subtree, not guaranteed across every root layout in the app. If you know your shared utility only ever runs under dashboard, that's fine — but if it's genuinely shared code that could execute from either subtree, you need the undefined check.

Catch-all and optional catch-all segments ([...path] and [[...path]]) work the same way you'd expect — path() resolves to string[] or string[] | undefined respectively.

Where It Doesn't Work

The restrictions here aren't arbitrary — they map directly onto where Next.js can and can't resolve a root parameter without an explicit request context.

Client Components are out entirely — this is a server-only module, enforced at build time, not just documented as a convention.

unstable_cache throws at runtime if you call a root parameter getter inside it. This one's worth remembering specifically because it won't fail at build time the way the Client Component restriction does — you'll only find out when the function actually executes. If you're caching something that needs a root parameter, use "use cache" instead; it's aware of which root parameters a function depends on in a way unstable_cache simply isn't.

Server Actions can't use it either, for a related reason: a Server Action can be invoked from any client, potentially long after the page that rendered it was generated, so there's no reliable "current route" to resolve the parameter against at call time.

Route Handlers are the one restriction that's temporary rather than architectural — support is explicitly planned for a future release, according to the docs. If you're building a Route Handler today that needs a root-level identifier, you're stuck reading it out of the request URL or headers manually for now.

A Practical Pattern: Locale-Aware Shared Utilities

The use case this module is obviously built for is internationalization, so it's worth seeing the full shape of that pattern. Say you have translation loading, a locale-aware date formatter, and a cached navigation query, all of which need to know the current language — but none of which are called from a single obvious place:

// lib/i18n.ts
import { lang } from "next/root-params";

export async function t(key: string): Promise<string> {
  const language = await lang();
  const dict = await import(`@/locales/${language}.json`);
  return dict[key] ?? key;
}

export async function formatDate(date: Date): Promise<string> {
  const language = await lang();
  return new Intl.DateTimeFormat(language).format(date);
}

Any Server Component anywhere under app/[lang]/ can now call await t("welcome_message") or await formatDate(post.publishedAt) without a single prop threaded through to get there. Before this module existed, the standard fix was a Context provider seeded with the locale in the root layout — which works, but forces every consumer to be a Client Component (or forces you to duplicate the value as a prop anyway for server-only consumers). next/root-params sidesteps that trade-off completely, because it never leaves the server.

Common Mistakes

Using a kebab-case segment name. A folder like app/[post-slug]/ will fail, because post-slug isn't a valid JavaScript identifier and can't be imported as a named export. Root parameters have to be valid function names — stick to camelCase or single words for any segment you intend to read this way.

Assuming it works in Route Handlers. This is the restriction people hit most often in practice, precisely because it's easy to forget it's not supported yet. If a Route Handler needs a value from a root segment, you currently have to parse it out of request.nextUrl.pathname yourself.

Forgetting the undefined case with multiple root layouts. If your project genuinely has more than one root layout, don't assume every root parameter getter always resolves to a concrete value — check the type TypeScript gives you, and handle the undefined branch instead of assuming it away.

Reaching for it when a normal route parameter would do. If the value only varies within a single branch of your route tree — not shared across the entire app — it's not a root parameter, and trying to force it into this pattern (by moving the segment up above the root layout just to use this API) usually creates more structural awkwardness than it saves.

Key Takeaways

QuestionAnswer
What is a root parameter?A dynamic segment that appears above your root layout — shared by every route under it
How do you read one?Import it by segment name from next/root-params and call it as an async function
Where can it be used?Server Components and server-side utilities only
Where can't it be used?Client Components, Server Actions, unstable_cache, and (for now) Route Handlers
What's the caching benefit?"use cache" functions only key on the root parameters they actually call, not every dynamic segment in the route
What if there are multiple root layouts?Getters for parameters not shared by every root layout return T | undefined

next/root-params is a small, single-purpose API, but it solves a real structural problem cleanly: it lets a value that's genuinely global to a subtree of your app behave like a global, without forcing you into Context providers or prop signatures that every layout in between has to carry along for the ride.

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