fix 'loading' segment not being copied into new CacheNode (#66538)

When `router.refresh` or a server action creates a new `CacheNode` tree,
we were erroneously not copying over the `loading` segment in the new
tree. This would cause the subtree to be remounted as the loading
segment switched from having a Suspense boundary to not having one.

Fixes #66029
Fixes #66499
This commit is contained in:
Zack Tanner 2024-06-04 10:15:42 -07:00 committed by GitHub
parent 58019b8684
commit 855ea3af24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 93 additions and 0 deletions

View file

@ -104,8 +104,10 @@ export function refreshReducer(
// Handles case where prefetch only returns the router tree patch without rendered components.
if (cacheNodeSeedData !== null) {
const rsc = cacheNodeSeedData[2]
const loading = cacheNodeSeedData[3]
cache.rsc = rsc
cache.prefetchRsc = null
cache.loading = loading
fillLazyItemsTillLeafWithHead(
cache,
// Existing cache is not passed in as `router.refresh()` has to invalidate the entire cache.

View file

@ -252,6 +252,7 @@ export function serverActionReducer(
const cache: CacheNode = createEmptyCacheNode()
cache.rsc = rsc
cache.prefetchRsc = null
cache.loading = cacheNodeSeedData[3]
fillLazyItemsTillLeafWithHead(
cache,
// Existing cache is not passed in as `router.refresh()` has to invalidate the entire cache.

View file

@ -0,0 +1,32 @@
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
describe('actions-revalidate-remount', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should not remount the page + loading component when revalidating', async () => {
const browser = await next.browser('/test')
const initialTime = await browser.elementById('time').text()
expect(initialTime).toMatch(/Time: \d+/)
await browser.elementByCss('button').click()
await retry(async () => {
const time = await browser.elementById('time').text()
expect(time).toMatch(/Time: \d+/)
// The time should be updated
expect(initialTime).not.toBe(time)
const logs = (await browser.log()).filter(
(log) => log.message === 'Loading Mounted'
)
// There should not be any loading logs
expect(logs.length).toBe(0)
})
})
})

View file

@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}

View file

@ -0,0 +1,11 @@
'use client'
import { useEffect } from 'react'
export default function Loading() {
useEffect(() => {
console.log('Root Loading Mounted')
}, [])
return <p>Root Page Loading</p>
}

View file

@ -0,0 +1,3 @@
export default function Page() {
return <p>hello world</p>
}

View file

@ -0,0 +1,11 @@
'use client'
import { useEffect } from 'react'
export default function Loading() {
useEffect(() => {
console.log('Loading Mounted')
}, [])
return <p>Test Page Loading</p>
}

View file

@ -0,0 +1,19 @@
import React from 'react'
import { revalidatePath } from 'next/cache'
export default async function HomePage() {
await new Promise((resolve) => setTimeout(resolve, 200))
return (
<div>
<p id="time">Time: {Date.now()}</p>
<form
action={async () => {
'use server'
revalidatePath('/test')
}}
>
<button>Submit</button>
</form>
</div>
)
}

View file

@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}
module.exports = nextConfig