Using next/root-params in Next.js 16.3
Aug 4, 2026 · by Jan Amann
Next.js v16.3 was just released and comes with a new feature: next/root-params.
This new API fills in the missing piece that allows apps that use top-level dynamic segments like [locale] to read segment values deeply in Server Components:
import {locale} from 'next/root-params';
async function Component() {
// The ability to read params deeply in
// Server Components ... finally!
const curLocale = await locale();
}This addition is a game-changer for next-intl.
While the library previously relied on workarounds to provide a locale to Server Components, this API now provides native support in Next.js for this use case, allowing the library to integrate much tighter with Next.js.
Practically, for users of next-intl this means:
- Being able to support static rendering of apps with locale-based routing without
setRequestLocale - Improved integration with Next.js cache mechanisms like
cacheComponents
But first, let’s have a look at how this API works in practice.
Introduction to root layouts
Prefer to watch a video?
→ Continue reading at static rendering
Previously, Next.js required a root layout to be present at app/layout.tsx—the root of your app.
Now, you can move a root layout to a nested segment, even if it’s a dynamic one:
src
└── app
└── [locale]
├── layout.tsx (root layout)
└── page.tsxIn this extended definition, a root layout now is any layout that has no other layouts located above it.
In contrast, layouts that do have other layout ancestors are regular layouts:
src
└── app
└── [locale]
├── layout.tsx (root layout)
├── (...)
└── news
├── layout.tsx (regular layout)
└── page.tsxWith the addition of next/root-params, you can now read param values of a root layout in all Server Components that render within it:
import {locale} from 'next/root-params';
export async function LocaleSwitcher() {
// Read the value of `[locale]`
const curLocale = await locale();
// ...
}Multiple root layouts
Here’s where it gets interesting: With route groups, you can provide another layout for pages that are not located in the [locale] segment:
src
└── app
├── [locale]
│ ├── layout.tsx
│ └── page.tsx
└── (unlocalized)
├── layout.tsx
└── page.tsxThe layout at [locale]/layout.tsx as well as the layout at (unlocalized)/layout.tsx both have no other layouts located above them, therefore both qualify as root layouts.
Due to this, in this case the returned value of next/root-params will depend on where the component that calls the function is being rendered from.
If you call next/root-params in shared code that is used by both layouts, this allows for a pattern like this:
import {locale} from 'next/root-params';
export default async function getLocale() {
// Try to read the locale in case we're in `[locale]/layout.tsx`
let curLocale = await locale();
// If we're in `(unlocalized)/layout.tsx`, let's use a fallback
if (!curLocale) {
curLocale = 'en';
}
return curLocale;
}With this, you can use the getLocale function across your codebase to read the current locale without having to worry about where it’s being called from.
In an internationalized app, this can for example be useful to implement a country selection page at the root where you have to rely on a default locale. Once the user is within the [locale] segment, this param value can be used instead for localizing page content.
Static rendering
In case we know the values for the [locale] segment ahead of time, we can provide them to Next.js by using the generateStaticParams function to enable static rendering:
const locales = ['en', 'de'];
// Pre-render all available locales at build time
export function generateStaticParams() {
return locales.map((locale) => ({locale}));
}
// ...Note that dynamicParams = false doesn’t work with Cache Components, therefore if you want to treat your locales array as exhaustive, you might be better off adding runtime validation for the [locale] segment and call notFound() to guard against unknown locales (see below).
Leveraging next/root-params in next-intl
So, how can you use this in next-intl?
Similarly to how we’ve defined the getLocale function above, we do in fact already have a central place that is called by all server-side functions that require the current locale of the user: i18n/request.ts.
So let’s use next/root-params here:
import * as rootParams from 'next/root-params';
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';
import {notFound} from 'next/navigation';
export default getRequestConfig(async () => {
const paramValue = await rootParams.locale();
let locale;
if (hasLocale(routing.locales, paramValue)) {
locale = paramValue;
} else {
// Runtime validation for unknown locales
notFound();
}
return {
locale
// ...
};
});That’s it—with a single change to i18n/request.ts you can start using next/root-params!
One caveat though: next/root-params currently doesn’t work in Route Handlers or Server Actions.
You can work around that though by passing an explicit locale param at relevant call sites:
async function action(locale: string) {
'use server';
const t = await getTranslations({locale, namespace: 'ContactForm'});
// ...
}… and then incorporate the override in getRequestConfig:
// ...
export default getRequestConfig(async ({locale}) => {
// Only read from `next/root-params` if no explicit
// override is provided by the caller
if (!locale) {
const paramValue = await rootParams.locale();
if (hasLocale(routing.locales, paramValue)) {
locale = paramValue;
} else {
notFound();
}
}
return {
locale
// ...
};
});Time for spring cleaning
With this change, you can now simplify your codebase in various ways:
Remove a pass-through root layout
For certain patterns like global 404 pages, you might have used a pass-through root layout so far:
export default function RootLayout({children}: LayoutProps<'/'>) {
return children;
}This needs to be removed now as otherwise this will qualify as a root layout instead of the one defined at src/app/[locale]/layout.tsx.
Instead, you can use global-not-found for this now.
Avoid reading the [locale] segment
Since next-intl provides the current locale via useLocale and getLocale, you can seamlessly read the locale from these APIs instead of params now:
+ import {getLocale} from 'next-intl/server';
export default async function RootLayout({
children,
- params
}: LayoutProps<'/[locale]'>) {
- const {locale} = await params;
+ const locale = await getLocale();
return (
<html lang={locale}>
<body>{children}</body>
</html>
);
}If you’ve augmented the Locale type you’ll now also benefit from type safety here:
// ✅ 'en' | 'de'
const locale = await getLocale();Behind the scenes, if you call useLocale or getLocale in a Server Component, your i18n/request.ts config will be consulted, potentially using a fallback that you have defined.
Remove manual locale overrides
If you’re using async APIs like getTranslations, you might have previously passed the locale manually, typically to enable static rendering in the Metadata API.
Now, you can remove this and rely on the locale that is returned from i18n/request.ts:
export async function generateMetadata(
- {params}: PageProps<'/[locale]'>
) {
- const {locale} = await params;
- const t = await getTranslations({locale, namespace: 'HomePage'});
+ const t = await getTranslations('HomePage');
// ...
}The cases where you still require a locale override are:
- When you’re using functions from
next-intlin Route Handlers or Server Actions (these are not supported bynext/root-params) - If your UI renders messages from multiple locales in parallel (uncommon)
Static rendering
If you’ve previously used setRequestLocale to enable static rendering, you can now remove it:
- import {setRequestLocale} from 'next-intl/server';
- export default function Page({params}: PageProps<'/[locale]'>) {
- setRequestLocale(params.locale);
+ export default function Page() {
// ...
}Note that generateStaticParams is naturally still required though.
Custom routing setups
next-intl provides mechanisms like localePrefix (and esp. prefixes) that allow you to customize your routing configuration. However, some apps might require further customization that goes beyond the capabilities that next-intl provides out of the box.
With the introduction of next/root-params, it’s now easier than ever to implement custom routing setups, while still being able to use core functionality from next-intl like useTranslations.
Example:
app/
└── [tenant]
├── layout.tsx
└── page.tsximport * as rootParams from 'next/root-params';
import {getRequestConfig} from 'next-intl/server';
import {fetchTenant} from '@/services/tenant';
export default getRequestConfig(async () => {
const tenantId = await rootParams.tenant();
const tenant = await fetchTenant(tenantId);
const locale = tenant.locale;
return {
locale
// ...
};
});In this case, you can consider implementing your own middleware and navigation APIs, if relevant.
Try next/root-params today!
If you’re giving next/root-params a go with next-intl, let me know how it works for you by joining the discussion here: Experiences with next/root-params.
I’m curious to hear how it simplifies your codebase!
—Jan
Want to ship a real-world app with next/root-params?
Master the art of holistic internationalization, from the fundamentals to advanced patterns, all through a real-world project.
Get started→