Skip to content

Commit 82de47f

Browse files
ceolinwillamannnclaude
authored
fix: Avoid usePathname() reads for non-locale-switching links, avoiding unnecessary <Suspense> wrappers with partial prefetching (#2362)
Co-authored-by: Jan Amann <jan@amann.work> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 89ae783 commit 82de47f

9 files changed

Lines changed: 97 additions & 50 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ dist/
88
.next/
99
tsconfig.tsbuildinfo
1010
.turbo
11-
.swc
11+
.swc
12+
.pnpm-store

e2e/partial-prefetching/src/app/[locale]/about/page.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
import {getTranslations, setRequestLocale} from 'next-intl/server';
1+
import {getTranslations} from 'next-intl/server';
22
import {Link} from '@/i18n/navigation';
33

4-
export default async function AboutPage({
5-
params
6-
}: PageProps<'/[locale]/about'>) {
7-
const {locale} = await params;
8-
setRequestLocale(locale);
4+
export default async function AboutPage() {
95
const t = await getTranslations('AboutPage');
106

117
return (
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {Link} from '@/i18n/navigation';
2+
3+
// The dynamic `[slug]` param ensures that the pathname of this
4+
// route is unknown when the shell is prerendered. Regardless of
5+
// this, `Link` should not require a `Suspense` boundary.
6+
export default function DynamicPage() {
7+
return (
8+
<main>
9+
<h1>Dynamic page</h1>
10+
<Link href="/">Go to home page</Link>
11+
</main>
12+
);
13+
}

e2e/partial-prefetching/src/app/[locale]/layout.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
1-
import {NextIntlClientProvider, hasLocale} from 'next-intl';
2-
import {setRequestLocale} from 'next-intl/server';
3-
import {notFound} from 'next/navigation';
1+
import {NextIntlClientProvider} from 'next-intl';
2+
import {getLocale} from 'next-intl/server';
43
import {routing} from '@/i18n/routing';
54

65
export function generateStaticParams() {
76
return routing.locales.map((locale) => ({locale}));
87
}
98

109
export default async function LocaleLayout({
11-
children,
12-
params
10+
children
1311
}: LayoutProps<'/[locale]'>) {
14-
const {locale} = await params;
15-
if (!hasLocale(routing.locales, locale)) {
16-
notFound();
17-
}
18-
setRequestLocale(locale);
12+
const locale = await getLocale();
1913

2014
return (
2115
<html lang={locale}>

e2e/partial-prefetching/src/app/[locale]/page.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import {getTranslations, setRequestLocale} from 'next-intl/server';
1+
import {getLocale, getTranslations} from 'next-intl/server';
22
import {Suspense} from 'react';
33
import LocaleCookieValue from '@/components/LocaleCookieValue';
44
import {Link} from '@/i18n/navigation';
55

6-
export default async function IndexPage({params}: PageProps<'/[locale]'>) {
7-
const {locale} = await params;
8-
setRequestLocale(locale);
6+
export default async function IndexPage() {
7+
const locale = await getLocale();
98
const t = await getTranslations('IndexPage');
109

1110
return (

e2e/partial-prefetching/src/i18n/request.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import {hasLocale} from 'next-intl';
22
import {getRequestConfig} from 'next-intl/server';
3+
import {notFound} from 'next/navigation';
4+
import * as rootParams from 'next/root-params';
35
import {routing} from './routing';
46

5-
export default getRequestConfig(async ({requestLocale}) => {
6-
const requested = await requestLocale;
7-
const locale = hasLocale(routing.locales, requested)
8-
? requested
9-
: routing.defaultLocale;
7+
export default getRequestConfig(async ({locale}) => {
8+
if (!locale) {
9+
const paramValue = await rootParams.locale();
10+
if (hasLocale(routing.locales, paramValue)) {
11+
locale = paramValue;
12+
} else {
13+
notFound();
14+
}
15+
}
1016

1117
return {
1218
locale,

e2e/partial-prefetching/tests/main.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,10 @@ it('supports navigation and locale switching', async ({page}) => {
3838
);
3939
expect(isSpa).toBe(true);
4040
});
41+
42+
it('supports a link in the shell of a dynamic route', async ({page}) => {
43+
await page.goto('/dynamic/example');
44+
await expect(page.getByRole('heading', {name: 'Dynamic page'})).toBeVisible();
45+
await page.getByRole('link', {name: 'Go to home page'}).click();
46+
await expect(page.getByRole('heading', {name: 'Home'})).toBeVisible();
47+
});

packages/next-intl/.size-limit.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ const config: SizeLimitConfig = [
2121
name: "import {createNavigation} from 'next-intl/navigation' (react-client)",
2222
path: 'dist/esm/production/navigation.react-client.js',
2323
import: '{createNavigation}',
24-
limit: '2.375 KB'
24+
limit: '2.4 KB'
2525
},
2626
{
2727
name: "import {createNavigation} from 'next-intl/navigation' (react-server)",
2828
path: 'dist/esm/production/navigation.react-server.js',
2929
import: '{createNavigation}',
30-
limit: '3.115 KB'
30+
limit: '3.155 KB'
3131
},
3232
{
3333
name: "import * from 'next-intl/server' (react-client)",

packages/next-intl/src/navigation/shared/BaseLink.tsx

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,32 @@ type Props = NextLinkProps & {
2121
localeCookie: InitializedLocaleCookieConfig;
2222
};
2323

24-
function BaseLink(
25-
{href, locale, localeCookie, onClick, prefetch, ...rest}: Props,
26-
ref: Ref<HTMLAnchorElement>
27-
) {
28-
const curLocale = useLocale();
29-
const isChangingLocale = locale != null && locale !== curLocale;
24+
type LocaleChangingLinkProps = NextLinkProps & {
25+
curLocale: Locale;
26+
linkRef: Ref<HTMLAnchorElement>;
27+
locale: Locale;
28+
localeCookie: InitializedLocaleCookieConfig;
29+
};
30+
31+
// Somehow the types for `next/link` don't work as expected
32+
// when `moduleResolution: "nodenext"` is used.
33+
const Link = NextLink as unknown as (props: NextLinkProps) => ReactNode;
3034

35+
// Links that change the locale are handled in a separate component,
36+
// since reading the pathname (necessary for syncing the locale cookie)
37+
// requires a Suspense boundary when Cache Components are used. Due to
38+
// this split, regular links are not subject to this requirement.
39+
function LocaleChangingLink({
40+
curLocale,
41+
linkRef,
42+
locale,
43+
localeCookie,
44+
onClick,
45+
prefetch,
46+
...rest
47+
}: LocaleChangingLinkProps) {
3148
// The types aren't entirely correct here. Outside of Next.js
32-
// `useParams` can be called, but the return type is `null`.
49+
// `usePathname` can be called, but the return type is `null`.
3350
const pathname = usePathname() as ReturnType<typeof usePathname> | null;
3451

3552
function onLinkClick(event: MouseEvent<HTMLAnchorElement>) {
@@ -40,29 +57,43 @@ function BaseLink(
4057
if (onClick) onClick(event);
4158
}
4259

43-
if (isChangingLocale) {
44-
if (prefetch && process.env.NODE_ENV !== 'production') {
45-
console.error(
46-
'The `prefetch` prop is currently not supported when using the `locale` prop on `Link` to switch the locale.`'
47-
);
48-
}
49-
prefetch = false;
60+
if (prefetch && process.env.NODE_ENV !== 'production') {
61+
console.error(
62+
'The `prefetch` prop is currently not supported when using the `locale` prop on `Link` to switch the locale.`'
63+
);
5064
}
5165

52-
// Somehow the types for `next/link` don't work as expected
53-
// when `moduleResolution: "nodenext"` is used.
54-
const Link = NextLink as unknown as (props: NextLinkProps) => ReactNode;
55-
5666
return (
5767
<Link
58-
ref={ref}
59-
href={href}
60-
hrefLang={isChangingLocale ? locale : undefined}
68+
ref={linkRef}
69+
hrefLang={locale}
6170
onClick={onLinkClick}
62-
prefetch={prefetch}
71+
prefetch={false}
6372
{...rest}
6473
/>
6574
);
6675
}
6776

77+
function BaseLink(
78+
{locale, localeCookie, ...rest}: Props,
79+
ref: Ref<HTMLAnchorElement>
80+
) {
81+
const curLocale = useLocale();
82+
const isChangingLocale = locale != null && locale !== curLocale;
83+
84+
if (isChangingLocale) {
85+
return (
86+
<LocaleChangingLink
87+
curLocale={curLocale}
88+
linkRef={ref}
89+
locale={locale}
90+
localeCookie={localeCookie}
91+
{...rest}
92+
/>
93+
);
94+
}
95+
96+
return <Link ref={ref} {...rest} />;
97+
}
98+
6899
export default forwardRef(BaseLink);

0 commit comments

Comments
 (0)