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

feat: rootParams (experimental) #72837

Merged
merged 1 commit into from
Dec 16, 2024
Merged

feat: rootParams (experimental) #72837

merged 1 commit into from
Dec 16, 2024

Conversation

ztanner
Copy link
Member

@ztanner ztanner commented Nov 15, 2024

What & Why

It's common to have top-level "dynamic" params that remain constant (as "global" or "root" params) regardless of where you are within a root layout. For example, [lang] or [locale], regardless of where you are in that layout that param will be available. We should provide a more convenient way to access these params without requiring it to be plumbed through ALS or context.

How

This introduces a new API, unstable_rootParams, that will return all segment params up to and including the root layout. In other words:

/app/[foo]/[bar]/layout.tsx -> { foo: string, bar: string }
/app/[foo]/[bar]/[baz]/page.tsx -> { foo: string, bar: string } (baz is not included here since the root params up to the root layout were just foo & bar.

This also supports the case of having multiple root layouts. Since navigating between a root layouts will trigger an MPA navigation, we're still able to enforce that those params will not change.

This PR also includes some work to the types plugin generate types for root params because they can be statically determined ahead of time. For example, in the above example, unstable_rootParams() will be typed as Promise<{ foo: string, bar: string }>.

In the case where there are multiple root layouts, it gets a bit more nuanced. e.g. at build time we aren't able to determine if you're going to be accessing rootParams on root layout A or root layout B. For this reason, they'll become optionally typed, eg: Promise<{ foo?: string }> where foo might only be available on the /app/(marketing)/[foo]/layout.tsx root.

This feature is experimental and under active development and as such, is currently exported with an unstable prefix.

@ztanner ztanner force-pushed the 11-12-feat_rootparams branch 2 times, most recently from 24269b3 to d509ed4 Compare November 15, 2024 00:52
@ijjk
Copy link
Member

ijjk commented Nov 15, 2024

Tests Passed

@ijjk
Copy link
Member

ijjk commented Nov 15, 2024

Stats from current PR

Default Build (Increase detected ⚠️)
General Overall increase ⚠️
vercel/next.js canary vercel/next.js 11-12-feat_rootparams Change
buildDuration 19.7s 17.4s N/A
buildDurationCached 16.6s 14.1s N/A
nodeModulesSize 410 MB 410 MB ⚠️ +125 kB
nextStartRea..uration (ms) 490ms 493ms N/A
Client Bundles (main, webpack)
vercel/next.js canary vercel/next.js 11-12-feat_rootparams Change
1187-HASH.js gzip 51 kB 51 kB N/A
8276.HASH.js gzip 169 B 168 B N/A
8377-HASH.js gzip 5.36 kB 5.36 kB N/A
bccd1874-HASH.js gzip 53 kB 53 kB N/A
framework-HASH.js gzip 57.5 kB 57.5 kB N/A
main-app-HASH.js gzip 232 B 235 B N/A
main-HASH.js gzip 34.1 kB 34 kB N/A
webpack-HASH.js gzip 1.71 kB 1.71 kB N/A
Overall change 0 B 0 B
Legacy Client Bundles (polyfills)
vercel/next.js canary vercel/next.js 11-12-feat_rootparams Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Overall change 39.4 kB 39.4 kB
Client Pages
vercel/next.js canary vercel/next.js 11-12-feat_rootparams Change
_app-HASH.js gzip 193 B 193 B
_error-HASH.js gzip 193 B 193 B
amp-HASH.js gzip 512 B 510 B N/A
css-HASH.js gzip 343 B 342 B N/A
dynamic-HASH.js gzip 1.84 kB 1.84 kB
edge-ssr-HASH.js gzip 265 B 265 B
head-HASH.js gzip 363 B 362 B N/A
hooks-HASH.js gzip 393 B 392 B N/A
image-HASH.js gzip 4.49 kB 4.49 kB N/A
index-HASH.js gzip 268 B 268 B
link-HASH.js gzip 2.35 kB 2.34 kB N/A
routerDirect..HASH.js gzip 328 B 328 B
script-HASH.js gzip 397 B 397 B
withRouter-HASH.js gzip 323 B 326 B N/A
1afbb74e6ecf..834.css gzip 106 B 106 B
Overall change 3.59 kB 3.59 kB
Client Build Manifests
vercel/next.js canary vercel/next.js 11-12-feat_rootparams Change
_buildManifest.js gzip 749 B 746 B N/A
Overall change 0 B 0 B
Rendered Page Sizes
vercel/next.js canary vercel/next.js 11-12-feat_rootparams Change
index.html gzip 524 B 524 B
link.html gzip 539 B 538 B N/A
withRouter.html gzip 519 B 520 B N/A
Overall change 524 B 524 B
Edge SSR bundle Size
vercel/next.js canary vercel/next.js 11-12-feat_rootparams Change
edge-ssr.js gzip 128 kB 128 kB N/A
page.js gzip 204 kB 204 kB N/A
Overall change 0 B 0 B
Middleware size
vercel/next.js canary vercel/next.js 11-12-feat_rootparams Change
middleware-b..fest.js gzip 671 B 668 B N/A
middleware-r..fest.js gzip 155 B 156 B N/A
middleware.js gzip 31.2 kB 31.2 kB N/A
edge-runtime..pack.js gzip 844 B 844 B
Overall change 844 B 844 B
Next Runtimes
vercel/next.js canary vercel/next.js 11-12-feat_rootparams Change
523-experime...dev.js gzip 322 B 322 B
523.runtime.dev.js gzip 314 B 314 B
app-page-exp...dev.js gzip 323 kB 323 kB N/A
app-page-exp..prod.js gzip 127 kB 127 kB N/A
app-page-tur..prod.js gzip 140 kB 140 kB N/A
app-page-tur..prod.js gzip 135 kB 135 kB N/A
app-page.run...dev.js gzip 314 kB 314 kB N/A
app-page.run..prod.js gzip 123 kB 123 kB N/A
app-route-ex...dev.js gzip 37.4 kB 37.4 kB N/A
app-route-ex..prod.js gzip 25.5 kB 25.5 kB N/A
app-route-tu..prod.js gzip 25.5 kB 25.5 kB N/A
app-route-tu..prod.js gzip 25.3 kB 25.3 kB N/A
app-route.ru...dev.js gzip 39 kB 39 kB N/A
app-route.ru..prod.js gzip 25.3 kB 25.3 kB N/A
pages-api-tu..prod.js gzip 9.69 kB 9.69 kB
pages-api.ru...dev.js gzip 11.6 kB 11.6 kB
pages-api.ru..prod.js gzip 9.68 kB 9.68 kB
pages-turbo...prod.js gzip 21.7 kB 21.7 kB
pages.runtim...dev.js gzip 27.4 kB 27.4 kB
pages.runtim..prod.js gzip 21.7 kB 21.7 kB
server.runti..prod.js gzip 916 kB 916 kB
Overall change 1.02 MB 1.02 MB
build cache Overall increase ⚠️
vercel/next.js canary vercel/next.js 11-12-feat_rootparams Change
0.pack gzip 2.06 MB 2.06 MB ⚠️ +5.33 kB
index.pack gzip 71.9 kB 72 kB ⚠️ +121 B
Overall change 2.13 MB 2.13 MB ⚠️ +5.45 kB
Diff details
Diff for middleware.js

Diff too large to display

Diff for edge-ssr.js

Diff too large to display

Diff for 1187-HASH.js

Diff too large to display

Diff for main-HASH.js

Diff too large to display

Diff for app-page-exp..ntime.dev.js

Diff too large to display

Diff for app-page-exp..time.prod.js

Diff too large to display

Diff for app-page-tur..time.prod.js

Diff too large to display

Diff for app-page-tur..time.prod.js

Diff too large to display

Diff for app-page.runtime.dev.js

Diff too large to display

Diff for app-page.runtime.prod.js

Diff too large to display

Diff for app-route-ex..ntime.dev.js

Diff too large to display

Diff for app-route-ex..time.prod.js

Diff too large to display

Diff for app-route-tu..time.prod.js

Diff too large to display

Diff for app-route-tu..time.prod.js

Diff too large to display

Diff for app-route.runtime.dev.js

Diff too large to display

Diff for app-route.ru..time.prod.js

Diff too large to display

Commit: 38766b5

@smuk3c
Copy link

smuk3c commented Nov 28, 2024

@ztanner is this one stale? 😞

Copy link
Member Author

ztanner commented Nov 28, 2024

@ztanner is this one stale? 😞

Still very much planned! Just being fleshed out a bit more with other things that we’re working on :)

@ztanner ztanner force-pushed the 11-12-feat_rootparams branch 2 times, most recently from 3df1ce1 to 4b7b241 Compare December 11, 2024 21:07
@wyattjoh wyattjoh force-pushed the 11-12-feat_rootparams branch 2 times, most recently from df2bca0 to 32957e5 Compare December 12, 2024 05:03
@wyattjoh wyattjoh force-pushed the 11-12-feat_rootparams branch from 32957e5 to ee411bc Compare December 14, 2024 02:04
@wyattjoh wyattjoh force-pushed the 11-12-feat_rootparams branch 2 times, most recently from 947da6a to 96ada8c Compare December 16, 2024 20:10
@ztanner ztanner changed the title (wip) feat: rootParams feat: rootParams (experimental) Dec 16, 2024
@ztanner ztanner force-pushed the 11-12-feat_rootparams branch from 96ada8c to 481684d Compare December 16, 2024 22:06
@ztanner ztanner marked this pull request as ready for review December 16, 2024 22:12
@wyattjoh wyattjoh force-pushed the 11-12-feat_rootparams branch from 481684d to 38766b5 Compare December 16, 2024 22:29
@wyattjoh wyattjoh merged commit f76c1ae into canary Dec 16, 2024
127 of 132 checks passed
Copy link
Member

Merge activity

  • Dec 16, 6:42 PM EST: A user merged this pull request with Graphite.

@amannn
Copy link
Contributor

amannn commented Dec 18, 2024

@ztanner I just did a few first experiments with rootParams in the latest canary and wanted to say that overall, from the use cases I've tried out so far, this works really really well! 👏👏

With different root layouts being possible, I found it really cool that you can also create app/(unlocalized)/page.tsx e.g. for a locale selection page.

Note that I've only used it in combination with stable rendering modes from Next.js so far, no dynamicIO & ppr (I saw that there are limitations).


One thing I noticed though is the handling of a global not-found page.

If you use dynamicParams = false or you call notFound() in app/[locale]/layout.tsx, you'll get this by default:

Screenshot 2024-12-18 at 17 48 36

If you add app/(unlocalized)/not-found.tsx along with a root layout, the same error is still shown.

If you add app/not-found.tsx, then you can get the page working in development, however the build will fail:

Screenshot 2024-12-18 at 17 25 03

Adding app/layout.tsx is not an option though since it will make rootParams unusable.

Intuitively, I'd think (unlocalized)/not-found.tsx would be great to get handled by Next.js, since it can reuse the layout from that folder.

Maybe that's something to consider? Not sure if I'm missing something. Here's a reproduction if you're interested.


Anyway, I'm incredibly happy that rootParams is coming to Next.js and am very much looking forward to the feature being available on the stable channel!

amannn added a commit to amannn/next-intl that referenced this pull request Dec 20, 2024
…d for overriding the locale (#1625)

**tldr;** — Do you use i18n routing and have you already switched to
[`await
requestLocale`](https://next-intl.dev/blog/next-intl-3-22#await-request-locale)
in `getRequestConfig`? If yes, you can skip this.

---

### Deprecation of `locale` in favor of `await requestLocale`

In [next-intl 3.22](https://next-intl.dev/blog/next-intl-3-22), the
`locale` argument that was passed to `getRequestConfig` was deprecated
in favor of [`await
requestLocale`](https://next-intl.dev/blog/next-intl-3-22#await-request-locale):

```diff
// i18n/request.ts

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

This change was done in preparation for Next.js 15 where reading from
headers [became
async](https://nextjs.org/blog/next-15#async-request-apis-breaking-change).
If you're using i18n routing, please upgrade to `requestLocale` now.

### Preview: `rootParams` are coming to Next.js

Now, with [`rootParams`](vercel/next.js#72837)
being on the horizon, this API will allow you to read a locale without
receiving any param passed to `getRequestConfig`:

```tsx
// 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;
 
  // ...
});
```

Among other simplifications, this allows to remove manual overrides like
this that were merely done for enabling static rendering:

```diff
- 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');
 
  // ...
}
```

However, in some rare cases, you might want to render messages from
multiple locales on the same page:

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

If you're using this pattern, you'll be able to accept the overridden
locale in `getRequestConfig` as follows:

```tsx
// 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 quite an edge case, but this use case will remain supported via
the re-introduced `locale` argument. Note that `await requestLocale`
considers a potential locale override, therefore the `locale` argument
will only be relevant once `rootParams` are a thing.

I hope to have more to share on this in the future!
@amannn
Copy link
Contributor

amannn commented Dec 20, 2024

@ztanner I had a bit more time to test and I think I found another bug.

If you have an app with a top-level [locale] segment and navigate within a given segment value (e.g. de) on the client side, then linked to pages will receive undefined as the segment value from rootParams.

Reproduction:

  1. Clone and run https://github.com/amannn/nextjs-rootparams-test in development
  2. Open http://localhost:3000/de ((await rootParams).locale === 'de' ✅)
  3. Click on a link to /de or /de/foo ((await rootParams).locale === undefined ❌)
  4. The default locale 'en' is used since rootParams has returned no value for locale

Note that with generateStaticParams this can be somewhat circumvented for a production build, since pages are correctly pre-rendered.

@ztanner
Copy link
Member Author

ztanner commented Dec 20, 2024

Thanks for the early feedback @amannn! We'll be taking a look at these as we work to stabilize the feature.

@amannn
Copy link
Contributor

amannn commented Dec 20, 2024

Sure, hope you didn’t mind the unsolicited feedback! I’m really excited for rootParams! 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants