Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: Add blog post about rootParams #1632

Draft
wants to merge 7 commits into
base: docs/v4-beta-blog-post
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/src/pages/blog/_meta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ export default {
index: {
title: 'Overview'
},
'nextjs-root-params': {
title: 'New in Next.js 15.X: rootParams',
display: 'hidden'
},
'next-intl-4-0': {
title: 'next-intl 4.0 beta',
display: 'hidden'
Expand Down
6 changes: 6 additions & 0 deletions docs/src/pages/blog/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import StayUpdated from '@/components/StayUpdated.mdx';
# next-intl blog

<div className="flex flex-col gap-4 py-8">
<BlogPostLink
href="/blog/nextjs-root-params"
title="New in Next.js 15.X: rootParams"
date="Dec XX, 2024"
author="By Jan Amann"
/>
<BlogPostLink
href="/blog/next-intl-4-0"
title="next-intl 4.0 beta"
Expand Down
2 changes: 1 addition & 1 deletion docs/src/pages/blog/next-intl-4-0.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ This led to three minor changes:

While the mentioned Next.js features are still under development and may change, these changes seem reasonable to me in any case—and ideally will be all that's necessary to adapt for `next-intl` to get the most out of these upcoming capabilities.

I'm particularly excited about the announcement of `rootParams`, as it seems like this will finally fill in the [missing piece](https://github.com/vercel/next.js/discussions/58862) that enables apps with i18n routing to support static rendering without workarounds like `setRequestLocale`. I hope to have more to share on this soon!
I'm particularly excited about the announcement of `rootParams`, as it seems like this will finally fill in the [missing piece](https://github.com/vercel/next.js/discussions/58862) that enables apps with i18n routing to support static rendering without workarounds like `setRequestLocale`. I'd encourage you to [give `rootParams` a try today](/blog/nextjs-root-params)!

## Other breaking changes

Expand Down
343 changes: 343 additions & 0 deletions docs/src/pages/blog/nextjs-root-params.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
---
title: 'New in Next.js 15.X: rootParams'
---

import StayUpdated from '@/components/StayUpdated.mdx';

# New in Next.js 15.X: `rootParams`

<small>Dec XX, 2024 · by Jan Amann</small>

(this post is still a draft)

Next.js v15.X was just released and with it comes a new feature: [`rootParams`](https://github.com/vercel/next.js/pull/72837).
amannn marked this conversation as resolved.
Show resolved Hide resolved

This new API fills in the [missing piece](https://github.com/vercel/next.js/discussions/58862) that allows apps that use top-level dynamic segments like `[locale]` to read segment values deeply in Server Components:

```tsx
import {unstable_rootParams as rootParams} from 'next/server';

async function Component() {
// The ability to read params deeply in
// Server Components ... finally!
const {locale} = await rootParams();
}
```

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:

1. Being able to support static rendering of apps with i18n routing without `setRequestLocale`
2. Improved integration with Next.js cache mechanisms
3. Preparation for upcoming rendering modes in Next.js like [Partial Prerendering](https://nextjs.org/docs/app/api-reference/next-config-js/ppr) and [`dynamicIO`](https://nextjs.org/docs/canary/app/api-reference/config/next-config-js/dynamicIO) (although there seems to be more work necessary here on the Next.js side)

But first, let's have a look at how this API works in practice.

## Root layouts

Previously, Next.js required a [root layout](https://nextjs.org/docs/app/api-reference/file-conventions/layout#root-layouts) to be present at `app/layout.tsx`.

Now, you can move these to a nested folder that can be a dynamic segment, e.g.:
amannn marked this conversation as resolved.
Show resolved Hide resolved

```
src
└── app
└── [locale]
├── layout.tsx (root layout)
└── page.tsx
```

A root layout is a 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
amannn marked this conversation as resolved.
Show resolved Hide resolved
├── (...)
└── news
├── layout.tsx (regular layout)
└── page.tsx
```

With the addition of `rootParams`, you can now read the param values of a root layout in all Server Components that render within it:
amannn marked this conversation as resolved.
Show resolved Hide resolved

```tsx filename=src/components/LocaleSwitcher.tsx
import {unstable_rootParams as rootParams} from 'next/server';

export async function LocaleSwitcher() {
// Read the value of `[locale]`
const {locale} = await rootParams();

// ...
}
```

## Multiple root layouts

Here's where it gets interesting: With [route groups](https://nextjs.org/docs/app/building-your-application/routing/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.tsx
```

The 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 `rootParams` will depend on where the component that calls the function is being rendered from.

If you call `rootParams` in shared code that is used by both root layouts, this allows for a pattern like this:

```tsx filename="src/utils/getLocale.tsx"
import {unstable_rootParams as rootParams} from 'next/server';

export async function getLocale() {
amannn marked this conversation as resolved.
Show resolved Hide resolved
// Try to read the locale in case we're in `[locale]/layout.tsx`
let {locale} = await rootParams();

// If we're in `(unlocalized)/layout.tsx`, let's use a fallback
if (!locale) {
locale = 'en';
}

return locale;
}
```

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.
amannn marked this conversation as resolved.
Show resolved Hide resolved

## Static rendering

In case we know all possible values for the `[locale]` segment ahead of time, we can provide them to Next.js using the [`generateStaticParams`](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) function to enable static rendering:

```tsx filename="src/app/[locale]/layout.tsx"
const locales = ['en', 'de'];

// Pre-render all available locales at build time
export async function generateStaticParams() {
return locales.map((locale) => ({locale}));
}

// ...
```

In combination with [`dynamicParams`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams), we can furthermore instruct Next.js to disallow values that are encountered at runtime and do not match the values we provided to `generateStaticParams`:
amannn marked this conversation as resolved.
Show resolved Hide resolved

```tsx filename="src/app/[locale]/layout.tsx"
// Return a 404 for any unknown locales
export const dynamicParams = false;

// ...
```

## Leveraging `rootParams` in `next-intl`

So, how can you use this feature in `next-intl`?

Similarly to how we've defined the `getLocale` function above, we do in fact already have a shared place that is called by all server-side functions that require the current locale of the user: [`i18n/request.ts`](/docs/usage/configuration#server-client-components).

So let's use `rootParams` here:

```tsx filename="src/i18n/request.ts"
import {unstable_rootParams as rootParams} from 'next/server';
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';

export default getRequestConfig(async () => {
const params = await rootParams();
const locale = hasLocale(routing.locales, params.locale)
? params.locale
: routing.defaultLocale;

return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});
```

`hasLocale` is a new addition scheduled for [`[email protected]`](/blog/next-intl-4-0), but practically simply checks if the provided `locales` array contains a given `locale`. If it doesn't, typically because we're not in the `[locale]` segment, a default locale is used instead.

That's it—a single change to `i18n/request.ts` is all you need to do to start using `rootParams`!

## So much cleaner
amannn marked this conversation as resolved.
Show resolved Hide resolved

With this change, you can now simplify your codebase in various ways:

### Removing a pass-through root layout

For certain patterns like global 404 pages, you might have used a pass-through root layout so far:

```tsx filename="src/app/layout.tsx"
type Props = {
children: React.ReactNode;
};

export default function RootLayout({children}: Props) {
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`.

### Avoid reading the `[locale]` segment

Since `next-intl` provides the current locale via [`useLocale` and `getLocale`](/docs/usage/configuration#locale), you can seamlessly read the locale from these APIs instead of `params` now:

```diff filename="src/app/[locale]/layout.tsx"
+ import {getLocale} from 'next-intl/server';

type Props = {
children: React.ReactNode;
- params: {locale: string};
};

export default async function RootLayout({
children,
- params
}: Props) {
- const {locale} = await params;
+ const locale = await getLocale();

return (
<html lang={locale}>
<body>{children}</body>
</html>
);
}
```

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've defined.
amannn marked this conversation as resolved.
Show resolved Hide resolved

### Remove manual locale overrides [#locale-override]

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`:

```diff filename="src/app/[locale]/page.tsx"
- type Props = {
- params: Promise<{locale: string}>;
- };

export async function generateMetadata(
- {params}: Props
) {
- const {locale} = await params;
- const t = await getTranslations({locale, namespace: 'HomePage'});
+ const t = await getTranslations('HomePage');

// ...
}
```

The only rare case where you might still want to pass a locale to `getTranslations` is if your UI renders messages from multiple locales in parallel:

```tsx
// Use messages from the current locale
const t = getTranslations();

// Use messages from 'en', regardless of what the current user locale is
const t = getTranslations({locale: 'en'});
```

In this case, you should make sure to accept an override in your `i18n/request.ts` config:

```tsx filename="src/i18n/request.ts"
import {unstable_rootParams as rootParams} from 'next/server';
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';

export default getRequestConfig(async ({locale}) => {
// Use a locale based on these priorities:
// 1. An override passed to the function
// 2. A locale from the `[locale]` segment
// 3. A default locale
if (!locale) {
const params = await rootParams();
locale = hasLocale(routing.locales, params.locale)
? params.locale
: routing.defaultLocale;
}

// ...
});
```

This is a very rare case, so if you're unsure, you very likely don't need this.

### Static rendering

If you've previously used `setRequestLocale` to enable static rendering, you can now remove it:

```diff filename="src/[locale]/page.tsx"
- import {setRequestLocale} from 'next-intl/server';

- type Props = {
- params: Promise<{locale: string}>;
- }

- export default function Page({params}: Props) {
- setRequestLocale(params.locale);
+ export default function Page() {
// ...
}
```

Note that `generateStaticParams` is naturally still required though.

### Handling unknown locales

Not strictly a new feature of Next.js, but in case you're using `generateStaticParams`, the easiest way to ensure that only the locales you've defined are allowed is to configure [`dynamicParams`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams) in your root layout:

```tsx filename="src/app/[locale]/layout.tsx"
// Return a 404 for any unknown locales
export const dynamicParams = false;
```

If you don't use `generateStaticParams`, you can still disallow unknown locales by manually calling `notFound()` based on `params` in your root layout:

```tsx filename="src/app/[locale]/layout.tsx"
import {hasLocale} from 'next-intl';
import {notFound} from 'next/navigation';
import {routing} from '@/i18n/routing';

type Props = {
children: React.ReactNode;
params: Promise<{locale: string}>;
};

export default async function RootLayout({children, params}: Props) {
const {locale} = await params;
if (!hasLocale(routing.locales, locale)) {
return notFound();
}

// ...
}
```

## Try `rootParams` today!

While this article mentions an upcoming `hasLocale` API from `[email protected]` that simplifies working with `rootParams`, you can already try out the API today even in the `3.0` range.

The one rare case where a change from `[email protected]` is required, is if you need to [manually pass a locale](#locale-override) to async APIs like `getTranslations` in case your UI renders multiple locales in parallel.

In case you're trying out `rootParams` with `next-intl`, let me know how it goes by joining the discussion here: [Experiences with `rootParams`](https://github.com/amannn/next-intl/discussions/1627). I'm curious to hear how it simplifies your codebase!
amannn marked this conversation as resolved.
Show resolved Hide resolved

—Jan

<StayUpdated />
Loading