Repository: vercel/next.js
Pull request: [Cache Components] Fix HMR for nested pages

To bypass existing caches when editing server components, we store an HMR hash in a session cookie. The cookie is sent to the server when refetching the RSC, and included in the cache key of all `'use cache'` functions (see #75474).

Before this fix, we weren't setting the cookie path properly, which led to multiple cookies for different paths being written and sent to the server. This caused stale values to be used for nested pages, which resulted in stale components being displayed after edits.

Now, we set the cookie path to `/`, ensuring that the same cookie is used for all nested pages.

closes NAR-204
resolves #81538

Changed files:
filename: packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx
patch: @@ -389,7 +389,7 @@ function processMessage(
 
       // Store the latest hash in a session cookie so that it's sent back to the
       // server with any subsequent requests.
-      document.cookie = `${NEXT_HMR_REFRESH_HASH_COOKIE}=${obj.hash}`
+      document.cookie = `${NEXT_HMR_REFRESH_HASH_COOKIE}=${obj.hash};path=/`
 
       if (RuntimeErrorHandler.hadRuntimeError) {
         if (reloading) return

filename: test/e2e/app-dir/use-cache-dev/app/page.tsx
patch: @@ -1,4 +1,5 @@
 import { revalidatePath } from 'next/cache'
+import Link from 'next/link'
 
 async function getRandomValue() {
   'use cache'
@@ -36,6 +37,9 @@ export default async function Page() {
       >
         <button id="revalidate">Revalidate</button>
       </form>
+      <p>
+        <Link href="/some/path">Go to /some/path</Link>
+      </p>
     </>
   )
 }

filename: test/e2e/app-dir/use-cache-dev/app/some/path/page.tsx
patch: @@ -0,0 +1,5 @@
+export default async function Page() {
+  'use cache'
+
+  return <p id="greeting">Hi</p>
+}

filename: test/e2e/app-dir/use-cache-dev/use-cache-dev.test.ts
patch: @@ -163,6 +163,33 @@ describe('use-cache-dev', () => {
         expect(next.cliOutput.slice(cliOutputLength)).toInclude('✓ Compiled')
       }, 10_000)
     })
+
+    it('should handle edits on nested pages', async () => {
+      // This is a regression test that ensures that setting the HMR refresh
+      // hash cookie is not done per path, leading to a stale cookie being used
+      // for nested pages.
+      const browser = await next.browser('/')
+
+      // Edit something in the root page.tsx file.
+      await next.patchFile('app/page.tsx', (content) =>
+        content.replace('foo', 'bar')
+      )
+
+      // Navigate to the nested page. This an explicit hard navigation. A soft
+      // navigation plus refresh would also reproduce the issue.
+      await browser.loadPage(new URL('/some/path', next.url).href)
+
+      expect(await browser.elementById('greeting').text()).toBe('Hi')
+
+      // Edit something in the nested page.tsx file.
+      await next.patchFile('app/some/path/page.tsx', (content) =>
+        content.replace('Hi', 'Hello')
+      )
+
+      await retry(async () => {
+        expect(await browser.elementById('greeting').text()).toBe('Hello')
+      })
+    })
   } else {
     it('should ignore an existing HMR refresh hash cookie with "next start"', async () => {
       const browser = await next.browser('/')