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

Prepare for async await headers() in Next.js 15 #1375

Open
2 tasks
Tracked by #1089
amannn opened this issue Sep 27, 2024 · 1 comment
Open
2 tasks
Tracked by #1089

Prepare for async await headers() in Next.js 15 #1375

amannn opened this issue Sep 27, 2024 · 1 comment
Labels
enhancement New feature or request

Comments

@amannn
Copy link
Owner

amannn commented Sep 27, 2024

Is your feature request related to a problem? Please describe.

Ref.: vercel/next.js#68812

next-intl currently relies on reading a request header to detect the current locale when relevant APIs are called in a Server Component. Note that this by itself is a workaround, ideally the locale could be retrieved from a param (see vercel/next.js#58862).

The APIs next-intl offers can be put in three categories:

  1. Formatting APIs (useTranslations & useFormatter)
  2. APIs for retrieving global configuration (e.g. useLocale)
  3. Navigation APIs (e.g. Link—these are lightweight wrappers for navigation APIs from Next.js and automatically incorporate localization of pathnames like locale prefixes)

The first two categories use APIs that have signatures that are either hooks or awaitable async functions—so those should be fine to update.

In the third category, the APIs are a bit more diverse. However, most APIs can be adapted as necessary:

  1. The Link component can be turned async to use await getLocale() internally
  2. useRouter & usePathname can consume a promise via use
  3. getPathname requires an explicit locale anyway

Only one API stands out as problematic: redirect

This is a function that can be called in the following environments:

  1. During the RSC render in an async component
  2. During the RSC render in a non-async component
  3. During SSR in a Client Component
  4. During CSR in a Client Component
  5. (Calling outside of render, e.g. in an event handler, will not have any effect)

Since use(…) doesn't work in async components, we'd have to use an awaitable API for (1). However, for the other call sites, a non-awaitable API would be more ergonomic (otherwise the user would have to wrap in use)

Describe the solution you'd like

I can think of the following solutions to address this:

  1. Require a locale to be passed to redirect: redirect({locale: 'en', href: '/about'}). To retrieve the locale, it'd be the job of the user to either call async getLocale() or useLocale() depending on the calling component.
    • Pros: Flexible, also adding the capability to redirect to other locales
    • Cons: More verbose to what we had before (i.e. redirect('/about')
  2. Make redirect awaitable
    • Pros: Can keep the ergonomics and not require a locale (mostly)
    • Cons: Divergence from what the user knows from Next.js (prone to errors), can not be used in non-async components

Another implication is likely that the user can't read the locale that's passed to i18n/request.ts synchronously. We might have to replace this with something promise-based (await locale or await getLocale()).

Open questions

  • If Next.js ever adds the ability to read params deeply, will this be async anyway? Since segment params became async, it seems so. Due to this, it's probably a good idea to plan long term and assume that accessing a locale will always be async.
  • Will hooks like use() work in the future in async RSC? If yes, then we could receive a default for the locale if we choose approach (1) from above and make the locale property optional. If we go for (2) we'd needlessly have an async API.

Moving forward

As matters stand currently, it seems like (1) will be more future-proof. Also, in case use() becomes available in async Server Components in the future, we can put back a default for the current locale in a later release.

Note that the navigation APIs are currently being reworked in #1316, so now would be a good point to adjust for the future.

@amannn amannn added the enhancement New feature or request label Sep 27, 2024
amannn added a commit that referenced this issue Oct 1, 2024
This PR provides a new **`createNavigation`** function that supersedes
the previously available APIs:
1. `createSharedPathnamesNavigation`
2. `createLocalizedPathnamesNavigation`

The new function unifies the API for both use cases and also fixes a few
quirks in the previous APIs.

**Usage**

```tsx
import {createNavigation} from 'next-intl/navigation';
import {defineRouting} from 'next-intl/routing';
 
export const routing = defineRouting(/* ... */);
 
export const {Link, redirect, usePathname, useRouter} =
  createNavigation(routing);
```

(see the [updated navigation
docs](https://next-intl-docs-git-feat-create-navigation-next-intl.vercel.app/docs/routing/navigation))

**Improvements**
1. A single API can be used both for shared as well as localized
pathnames. This reduces the API surface and simplifies the corresponding
docs.
2. `Link` can now be composed seamlessly into another component with its
`href` prop without having to add a generic type argument.
3. `getPathname` is now available for both shared as well as localized
pathnames (fixes #785)
4. `router.push` and `redirect` now accept search params consistently
via the object form (e.g. `router.push({pathname: '/users', query:
{sortBy: 'name'})`)—regardless of if you're using shared or localized
pathnames.
5. When using `localePrefix: 'as-necessary'`, the initial render of
`Link` now uses the correct pathname immediately during SSR (fixes
[#444](#444)). Previously, a
prefix for the default locale was added during SSR and removed during
hydration. Also `redirect` now gets the final pathname right without
having to add a superfluous prefix (fixes
[#1335](#1335)). The only
exception is when you use `localePrefix: 'as-necessary'` in combination
with `domains` (see [Special case: Using `domains` with `localePrefix:
'as-needed'`](https://next-intl-docs-git-feat-create-navigation-next-intl.vercel.app/docs/routing#domains-localeprefix-asneeded))
6. `Link` is now compatible with the `asChild` prop of Radix Primitives
when rendered in RSC (see
[#1322](#1322))

**Migrating to `createNavigation`**

`createNavigation` is generally considered a drop-in replacement, but a
few changes might be necessary:
1. `createNavigation` is expected to receive your complete routing
configuration. Ideally, you define this via the
[`defineRouting`](https://next-intl-docs.vercel.app/docs/routing#define-routing)
function and pass the result to `createNavigation`.
2. If you've used `createLocalizedPathnamesNavigation` and have
[composed the `Link` with its `href`
prop](https://next-intl-docs.vercel.app/docs/routing/navigation#link-composition),
you should no longer provide the generic `Pathname` type argument (see
[updated
docs](https://next-intl-docs-git-feat-create-navigation-next-intl.vercel.app/docs/routing/navigation#link-composition)).
```diff
- ComponentProps<typeof Link<Pathname>>
+ ComponentProps<typeof Link>
```
3. If you've used
[`redirect`](https://next-intl-docs.vercel.app/docs/routing/navigation#redirect),
you now have to provide an explicit locale (even if it's just [the
current
locale](https://next-intl-docs.vercel.app/docs/usage/configuration#locale)).
This change was necessary for an upcoming change in Next.js 15 where
`headers()` turns into a promise (see
[#1375](#1375) for details).
```diff
- redirect('/about')
+ redirect({pathname: '/about', locale: 'en'})
```
4. If you've used
[`getPathname`](https://next-intl-docs.vercel.app/docs/routing/navigation#getpathname)
and have previously manually prepended a locale prefix, you should no
longer do so—`getPathname` now takes care of this depending on your
routing strategy.
```diff
- '/'+ locale + getPathname(/* ... */)
+ getPathname(/* ... */);
```
5. If you're using a combination of `localePrefix: 'as-necessary'` and
`domains` and you're using `getPathname`, you now need to provide a
`domain` argument (see [Special case: Using `domains` with
`localePrefix:
'as-needed'`](https://next-intl-docs-git-feat-create-navigation-next-intl.vercel.app/docs/routing#domains-localeprefix-asneeded))
@amannn
Copy link
Owner Author

amannn commented Oct 1, 2024

amannn added a commit that referenced this issue Oct 2, 2024
…js 15 support (#1383)

Since [Next.js is switching `headers()` to be
`async`](vercel/next.js#68812), the `locale`
that is passed to `getRequestConfig` needs to be replaced by an
awaitable alternative. Note that this is only relevant for your app in
case you're using i18n routing.

## tldr;

Switch to the new API and call it a day:

```diff
export default getRequestConfig(async ({
-  locale
+  requestLocale
}) => {
+  const locale = await requestLocale;

  // ...
});
```

If your app worked well before, then this is a 1:1 switch and will get
your app in shape for Next.js 15.

## Details

The new `requestLocale` parameter also offered a chance to get in some
enhancements for edge cases that were previously harder to support.
Therefore, the following migration is generally recommended:

**Before:**

```tsx
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
import {routing} from './routing';
 
export default getRequestConfig(async ({locale}) => {
  // Validate that the incoming `locale` parameter is valid
  if (!routing.locales.includes(locale as any)) notFound();
 
  return {
    // ...
  };
});
```

**After:**

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

export default getRequestConfig(async ({requestLocale}) => {
  // This typically corresponds to the `[locale]` segment
  let locale = await requestLocale;

  // Ensure that the incoming locale is valid
  if (!locale || !routing.locales.includes(locale as any)) {
    locale = routing.defaultLocale;
  }

  return {
    locale,
    // ...
  };
});
```

The differences are:
1. `requestLocale` is a promise that needs to be awaited
2. The resolved value can be `undefined`—therefore a default should be
supplied. The default assignment allows handling cases where an error
would be thrown previously, e.g. when using APIs like `useTranslations`
on a global language selection page at `app/page.tsx`.
3. The `locale` should be returned (since you can now adjust it in the
function body).
4. We now recommend calling `notFound()` in response to an invalid
`[locale]` param in
[`app/[locale]/layout.tsx`](https://next-intl-docs-git-feat-async-request-locale-next-intl.vercel.app/docs/getting-started/app-router/with-i18n-routing#layout)
instead of in `i18n/request.ts`. This unlocks another use case, where
APIs like `useTranslations` can now be used on a global
`app/not-found.tsx` page.

See also the [updated getting started
docs](https://next-intl-docs-git-feat-async-request-locale-next-intl.vercel.app/docs/getting-started/app-router/with-i18n-routing#i18n-request).

Note that this change is non-breaking, but the synchronously available
`locale` is now considered deprecated and will be removed in a future
major version.

Contributes to #1375
Addresses #1355
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant