Repository: vercel/next.js
Pull request: Sync devFallbackParams when generateStaticParams change

Since `generateStaticParams` runs in the background, when accessing the `devFallbackParams` during the render, it is still set to the previous result from the cache. Therefore, when the result has changed, re-render the RSC to sync the `devFallbackParams` with the new result.

Closes NAR-491

Changed files:
filename: packages/next/src/server/app-render/app-render.tsx
patch: @@ -2096,8 +2096,7 @@ async function renderToHTMLOrFlightImpl(
       null
 
     const rootParams = getRootParams(loaderTree, ctx.getDynamicParamFromSegment)
-    const devValidatingFallbackParams =
-      getRequestMeta(req, 'devValidatingFallbackParams') || null
+    const devFallbackParams = getRequestMeta(req, 'devFallbackParams') || null
 
     const createRequestStore = createRequestStoreForRender.bind(
       null,
@@ -2111,7 +2110,7 @@ async function renderToHTMLOrFlightImpl(
       isHmrRefresh,
       serverComponentsHmrCache,
       renderResumeDataCache,
-      devValidatingFallbackParams
+      devFallbackParams
     )
     const requestStore = createRequestStore()
 
@@ -2201,7 +2200,7 @@ async function renderToHTMLOrFlightImpl(
             postponedState,
             metadata,
             undefined, // Prevent restartable-render behavior in dev + Cache Components mode
-            devValidatingFallbackParams
+            devFallbackParams
           )
 
           return new RenderResult(stream, {
@@ -2245,7 +2244,7 @@ async function renderToHTMLOrFlightImpl(
       // and we currently we don't copy changes over when creating a new store,
       // so the restarted render wouldn't be correct.
       didExecuteServerAction ? undefined : createRequestStore,
-      devValidatingFallbackParams
+      devFallbackParams
     )
 
     // Invalid dynamic usages should only error the request in development.
@@ -2451,7 +2450,7 @@ async function renderToStream(
   postponedState: PostponedState | null,
   metadata: AppPageRenderResultMetadata,
   createRequestStore: (() => RequestStore) | undefined,
-  devValidatingFallbackParams: OpaqueFallbackRouteParams | null
+  devFallbackParams: OpaqueFallbackRouteParams | null
 ): Promise<ReadableStream<Uint8Array>> {
   /* eslint-disable @next/internal/no-ambiguous-jsx -- React Client */
   const {
@@ -2689,7 +2688,7 @@ async function renderToStream(
         res.statusCode === 404,
         clientReferenceManifest,
         requestStore,
-        devValidatingFallbackParams
+        devFallbackParams
       )
     } else {
       // This is a dynamic render. We don't do dynamic tracking because we're not prerendering

filename: packages/next/src/server/base-server.ts
patch: @@ -2298,7 +2298,7 @@ export default abstract class Server<
           if (smallestFallbackRouteParams) {
             addRequestMeta(
               req,
-              'devValidatingFallbackParams',
+              'devFallbackParams',
               createOpaqueFallbackRouteParams(smallestFallbackRouteParams)!
             )
           }

filename: packages/next/src/server/dev/next-dev-server.ts
patch: @@ -76,6 +76,7 @@ import {
 import type { PrerenderManifest } from '../../build'
 import { getRouteRegex } from '../../shared/lib/router/utils/route-regex'
 import type { PrerenderedRoute } from '../../build/static-paths/types'
+import { HMR_MESSAGE_SENT_TO_BROWSER } from './hot-reloader-types'
 
 // Load ReactDevOverlay only when needed
 let PagesDevOverlayBridgeImpl: PagesDevOverlayBridgeType
@@ -802,6 +803,24 @@ export default class DevServer extends Server {
               )
             }
           }
+
+          // Since generateStaticParams run on the background, when accessing the
+          // devFallbackParams during the render, it is still set to the previous
+          // result from the cache. Therefore when the result has changed, re-render
+          // the Server Component to sync the devFallbackParams with the new result.
+          if (
+            isAppPath &&
+            this.nextConfig.cacheComponents &&
+            // Ensure this is not the first invocation.
+            result &&
+            // Ideally, we would want to compare the whole objects, but that is too expensive.
+            result.prerenderedRoutes?.length !== prerenderedRoutes?.length
+          ) {
+            this.bundlerService.triggerHMR({
+              type: HMR_MESSAGE_SENT_TO_BROWSER.SERVER_COMPONENT_CHANGES,
+              hash: `generateStaticParams-${Date.now()}`,
+            })
+          }
         }
 
         if (!isAppPath && this.nextConfig.output === 'export') {

filename: packages/next/src/server/lib/dev-bundler-service.ts
patch: @@ -4,7 +4,10 @@ import type { WorkerRequestHandler } from './types'
 
 import { LRUCache } from './lru-cache'
 import { createRequestResponseMocks } from './mock-request'
-import { HMR_MESSAGE_SENT_TO_BROWSER } from '../dev/hot-reloader-types'
+import {
+  HMR_MESSAGE_SENT_TO_BROWSER,
+  type HmrMessageSentToBrowser,
+} from '../dev/hot-reloader-types'
 import type { ReactDebugChannelForBrowser } from '../dev/debug-channel'
 import type { ServerCacheStatus } from '../../next-devtools/dev-overlay/cache-indicator'
 
@@ -137,4 +140,8 @@ export class DevBundlerService {
   public close() {
     this.bundler.hotReloader.close()
   }
+
+  public triggerHMR(message: HmrMessageSentToBrowser) {
+    this.bundler.hotReloader.send(message)
+  }
 }

filename: packages/next/src/server/request-meta.ts
patch: @@ -261,7 +261,7 @@ export interface RequestMeta {
   /**
    * DEV only: The fallback params that should be used when validating prerenders during dev
    */
-  devValidatingFallbackParams?: OpaqueFallbackRouteParams
+  devFallbackParams?: OpaqueFallbackRouteParams
 
   /**
    * DEV only: Request timings in process.hrtime.bigint()