Skip to content

Commit 80055fd

Browse files
authored
fix(hydration): skip lazy hydration for patched components (#13283)
close #13255
1 parent b991075 commit 80055fd

File tree

3 files changed

+53
-3
lines changed

3 files changed

+53
-3
lines changed

packages/runtime-core/src/apiAsyncComponent.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
type ComponentOptions,
55
type ConcreteComponent,
66
currentInstance,
7+
getComponentName,
78
isInSSRComponentSetup,
89
} from './component'
910
import { isFunction, isObject } from '@vue/shared'
@@ -121,14 +122,27 @@ export function defineAsyncComponent<
121122
__asyncLoader: load,
122123

123124
__asyncHydrate(el, instance, hydrate) {
125+
let patched = false
124126
const doHydrate = hydrateStrategy
125127
? () => {
126-
const teardown = hydrateStrategy(hydrate, cb =>
128+
const performHydrate = () => {
129+
// skip hydration if the component has been patched
130+
if (__DEV__ && patched) {
131+
warn(
132+
`Skipping lazy hydration for component '${getComponentName(resolvedComp!)}': ` +
133+
`it was updated before lazy hydration performed.`,
134+
)
135+
return
136+
}
137+
hydrate()
138+
}
139+
const teardown = hydrateStrategy(performHydrate, cb =>
127140
forEachElement(el, cb),
128141
)
129142
if (teardown) {
130143
;(instance.bum || (instance.bum = [])).push(teardown)
131144
}
145+
;(instance.u || (instance.u = [])).push(() => (patched = true))
132146
}
133147
: hydrate
134148
if (resolvedComp) {

packages/vue/__tests__/e2e/hydration-strat-media.html

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@
1515
} = Vue
1616

1717
const Comp = {
18-
setup() {
18+
props: {
19+
value: Boolean,
20+
},
21+
setup(props) {
1922
const count = ref(0)
2023
onMounted(() => {
2124
console.log('hydrated')
2225
window.isHydrated = true
2326
})
2427
return () => {
28+
props.value
2529
return h('button', { onClick: () => count.value++ }, count.value)
2630
}
2731
},
@@ -37,7 +41,9 @@
3741
onMounted(() => {
3842
window.isRootMounted = true
3943
})
40-
return () => h(AsyncComp)
44+
45+
const show = (window.show = ref(true))
46+
return () => h(AsyncComp, { value: show.value })
4147
},
4248
}).mount('#app')
4349
</script>

packages/vue/__tests__/e2e/hydrationStrategies.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,36 @@ describe('async component hydration strategies', () => {
8686
await assertHydrationSuccess()
8787
})
8888

89+
// #13255
90+
test('media query (patched before hydration)', async () => {
91+
const spy = vi.fn()
92+
const currentPage = page()
93+
currentPage.on('pageerror', spy)
94+
95+
const warn: any[] = []
96+
currentPage.on('console', e => warn.push(e.text()))
97+
98+
await goToCase('media')
99+
await page().waitForFunction(() => window.isRootMounted)
100+
expect(await page().evaluate(() => window.isHydrated)).toBe(false)
101+
102+
// patch
103+
await page().evaluate(() => (window.show.value = false))
104+
await click('button')
105+
expect(await text('button')).toBe('1')
106+
107+
// resize
108+
await page().setViewport({ width: 400, height: 600 })
109+
await page().waitForFunction(() => window.isHydrated)
110+
await assertHydrationSuccess('2')
111+
112+
expect(spy).toBeCalledTimes(0)
113+
currentPage.off('pageerror', spy)
114+
expect(
115+
warn.some(w => w.includes('Skipping lazy hydration for component')),
116+
).toBe(true)
117+
})
118+
89119
test('interaction', async () => {
90120
await goToCase('interaction')
91121
await page().waitForFunction(() => window.isRootMounted)

0 commit comments

Comments
 (0)