Skip to content

Commit 0a7932c

Browse files
committed
fix(ssr): should set ref on hydration
1 parent 5a3b44c commit 0a7932c

File tree

3 files changed

+135
-107
lines changed

3 files changed

+135
-107
lines changed

packages/runtime-core/__tests__/hydration.spec.ts

+9
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,15 @@ describe('SSR hydration', () => {
124124
expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
125125
})
126126

127+
test('element with ref', () => {
128+
const el = ref()
129+
const { vnode, container } = mountWithHydration('<div></div>', () =>
130+
h('div', { ref: el })
131+
)
132+
expect(vnode.el).toBe(container.firstChild)
133+
expect(el.value).toBe(vnode.el)
134+
})
135+
127136
test('Fragment', async () => {
128137
const msg = ref('foo')
129138
const fn = jest.fn()

packages/runtime-core/src/hydration.ts

+76-59
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ComponentOptions, ComponentInternalInstance } from './component'
1212
import { invokeDirectiveHook } from './directives'
1313
import { warn } from './warning'
1414
import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared'
15-
import { RendererInternals, invokeVNodeHook } from './renderer'
15+
import { RendererInternals, invokeVNodeHook, setRef } from './renderer'
1616
import {
1717
SuspenseImpl,
1818
SuspenseBoundary,
@@ -88,74 +88,85 @@ export function createHydrationFunctions(
8888
isFragmentStart
8989
)
9090

91-
const { type, shapeFlag } = vnode
91+
const { type, ref, shapeFlag } = vnode
9292
const domType = node.nodeType
9393
vnode.el = node
9494

95+
let nextNode: Node | null = null
9596
switch (type) {
9697
case Text:
9798
if (domType !== DOMNodeTypes.TEXT) {
98-
return onMismatch()
99-
}
100-
if ((node as Text).data !== vnode.children) {
101-
hasMismatch = true
102-
__DEV__ &&
103-
warn(
104-
`Hydration text mismatch:` +
105-
`\n- Client: ${JSON.stringify((node as Text).data)}` +
106-
`\n- Server: ${JSON.stringify(vnode.children)}`
107-
)
108-
;(node as Text).data = vnode.children as string
99+
nextNode = onMismatch()
100+
} else {
101+
if ((node as Text).data !== vnode.children) {
102+
hasMismatch = true
103+
__DEV__ &&
104+
warn(
105+
`Hydration text mismatch:` +
106+
`\n- Client: ${JSON.stringify((node as Text).data)}` +
107+
`\n- Server: ${JSON.stringify(vnode.children)}`
108+
)
109+
;(node as Text).data = vnode.children as string
110+
}
111+
nextNode = nextSibling(node)
109112
}
110-
return nextSibling(node)
113+
break
111114
case Comment:
112115
if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) {
113-
return onMismatch()
116+
nextNode = onMismatch()
117+
} else {
118+
nextNode = nextSibling(node)
114119
}
115-
return nextSibling(node)
120+
break
116121
case Static:
117122
if (domType !== DOMNodeTypes.ELEMENT) {
118-
return onMismatch()
119-
}
120-
// determine anchor, adopt content
121-
let cur = node
122-
// if the static vnode has its content stripped during build,
123-
// adopt it from the server-rendered HTML.
124-
const needToAdoptContent = !(vnode.children as string).length
125-
for (let i = 0; i < vnode.staticCount; i++) {
126-
if (needToAdoptContent) vnode.children += (cur as Element).outerHTML
127-
if (i === vnode.staticCount - 1) {
128-
vnode.anchor = cur
123+
nextNode = onMismatch()
124+
} else {
125+
// determine anchor, adopt content
126+
nextNode = node
127+
// if the static vnode has its content stripped during build,
128+
// adopt it from the server-rendered HTML.
129+
const needToAdoptContent = !(vnode.children as string).length
130+
for (let i = 0; i < vnode.staticCount; i++) {
131+
if (needToAdoptContent)
132+
vnode.children += (nextNode as Element).outerHTML
133+
if (i === vnode.staticCount - 1) {
134+
vnode.anchor = nextNode
135+
}
136+
nextNode = nextSibling(nextNode)!
129137
}
130-
cur = nextSibling(cur)!
138+
return nextNode
131139
}
132-
return cur
140+
break
133141
case Fragment:
134142
if (!isFragmentStart) {
135-
return onMismatch()
143+
nextNode = onMismatch()
144+
} else {
145+
nextNode = hydrateFragment(
146+
node as Comment,
147+
vnode,
148+
parentComponent,
149+
parentSuspense,
150+
optimized
151+
)
136152
}
137-
return hydrateFragment(
138-
node as Comment,
139-
vnode,
140-
parentComponent,
141-
parentSuspense,
142-
optimized
143-
)
153+
break
144154
default:
145155
if (shapeFlag & ShapeFlags.ELEMENT) {
146156
if (
147157
domType !== DOMNodeTypes.ELEMENT ||
148158
vnode.type !== (node as Element).tagName.toLowerCase()
149159
) {
150-
return onMismatch()
160+
nextNode = onMismatch()
161+
} else {
162+
nextNode = hydrateElement(
163+
node as Element,
164+
vnode,
165+
parentComponent,
166+
parentSuspense,
167+
optimized
168+
)
151169
}
152-
return hydrateElement(
153-
node as Element,
154-
vnode,
155-
parentComponent,
156-
parentSuspense,
157-
optimized
158-
)
159170
} else if (shapeFlag & ShapeFlags.COMPONENT) {
160171
// when setting up the render effect, if the initial vnode already
161172
// has .el set, the component will perform hydration instead of mount
@@ -182,24 +193,25 @@ export function createHydrationFunctions(
182193
// component may be async, so in the case of fragments we cannot rely
183194
// on component's rendered output to determine the end of the fragment
184195
// instead, we do a lookahead to find the end anchor node.
185-
return isFragmentStart
196+
nextNode = isFragmentStart
186197
? locateClosingAsyncAnchor(node)
187198
: nextSibling(node)
188199
} else if (shapeFlag & ShapeFlags.TELEPORT) {
189200
if (domType !== DOMNodeTypes.COMMENT) {
190-
return onMismatch()
201+
nextNode = onMismatch()
202+
} else {
203+
nextNode = (vnode.type as typeof TeleportImpl).hydrate(
204+
node,
205+
vnode,
206+
parentComponent,
207+
parentSuspense,
208+
optimized,
209+
rendererInternals,
210+
hydrateChildren
211+
)
191212
}
192-
return (vnode.type as typeof TeleportImpl).hydrate(
193-
node,
194-
vnode,
195-
parentComponent,
196-
parentSuspense,
197-
optimized,
198-
rendererInternals,
199-
hydrateChildren
200-
)
201213
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
202-
return (vnode.type as typeof SuspenseImpl).hydrate(
214+
nextNode = (vnode.type as typeof SuspenseImpl).hydrate(
203215
node,
204216
vnode,
205217
parentComponent,
@@ -212,8 +224,13 @@ export function createHydrationFunctions(
212224
} else if (__DEV__) {
213225
warn('Invalid HostVNode type:', type, `(${typeof type})`)
214226
}
215-
return null
216227
}
228+
229+
if (ref != null && parentComponent) {
230+
setRef(ref, null, parentComponent, vnode)
231+
}
232+
233+
return nextNode
217234
}
218235

219236
const hydrateElement = (
@@ -386,7 +403,7 @@ export function createHydrationFunctions(
386403
parentComponent: ComponentInternalInstance | null,
387404
parentSuspense: SuspenseBoundary | null,
388405
isFragment: boolean
389-
) => {
406+
): Node | null => {
390407
hasMismatch = true
391408
__DEV__ &&
392409
warn(

packages/runtime-core/src/renderer.ts

+50-48
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity'
4747
import { updateProps } from './componentProps'
4848
import { updateSlots } from './componentSlots'
4949
import { pushWarningContext, popWarningContext, warn } from './warning'
50-
import { ComponentPublicInstance } from './componentProxy'
5150
import { createAppAPI, CreateAppFunction } from './apiCreateApp'
5251
import {
5352
SuspenseBoundary,
@@ -271,6 +270,55 @@ export const queuePostRenderEffect = __FEATURE_SUSPENSE__
271270
? queueEffectWithSuspense
272271
: queuePostFlushCb
273272

273+
export const setRef = (
274+
rawRef: VNodeNormalizedRef,
275+
oldRawRef: VNodeNormalizedRef | null,
276+
parent: ComponentInternalInstance,
277+
vnode: VNode | null
278+
) => {
279+
const value = vnode
280+
? vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
281+
? vnode.component!.proxy
282+
: vnode.el
283+
: null
284+
const [owner, ref] = rawRef
285+
if (__DEV__ && !owner) {
286+
warn(
287+
`Missing ref owner context. ref cannot be used on hoisted vnodes. ` +
288+
`A vnode with ref must be created inside the render function.`
289+
)
290+
return
291+
}
292+
const oldRef = oldRawRef && oldRawRef[1]
293+
const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
294+
const setupState = owner.setupState
295+
296+
// unset old ref
297+
if (oldRef != null && oldRef !== ref) {
298+
if (isString(oldRef)) {
299+
refs[oldRef] = null
300+
if (hasOwn(setupState, oldRef)) {
301+
setupState[oldRef] = null
302+
}
303+
} else if (isRef(oldRef)) {
304+
oldRef.value = null
305+
}
306+
}
307+
308+
if (isString(ref)) {
309+
refs[ref] = value
310+
if (hasOwn(setupState, ref)) {
311+
setupState[ref] = value
312+
}
313+
} else if (isRef(ref)) {
314+
ref.value = value
315+
} else if (isFunction(ref)) {
316+
callWithErrorHandling(ref, parent, ErrorCodes.FUNCTION_REF, [value, refs])
317+
} else if (__DEV__) {
318+
warn('Invalid template ref type:', value, `(${typeof value})`)
319+
}
320+
}
321+
274322
/**
275323
* The createRenderer function accepts two generic arguments:
276324
* HostNode and HostElement, corresponding to Node and Element types in the
@@ -440,9 +488,7 @@ function baseCreateRenderer(
440488

441489
// set ref
442490
if (ref != null && parentComponent) {
443-
const refValue =
444-
shapeFlag & ShapeFlags.STATEFUL_COMPONENT ? n2.component!.proxy : n2.el
445-
setRef(ref, n1 && n1.ref, parentComponent, refValue)
491+
setRef(ref, n1 && n1.ref, parentComponent, n2)
446492
}
447493
}
448494

@@ -1984,50 +2030,6 @@ function baseCreateRenderer(
19842030
return hostNextSibling((vnode.anchor || vnode.el)!)
19852031
}
19862032

1987-
const setRef = (
1988-
rawRef: VNodeNormalizedRef,
1989-
oldRawRef: VNodeNormalizedRef | null,
1990-
parent: ComponentInternalInstance,
1991-
value: RendererNode | ComponentPublicInstance | null
1992-
) => {
1993-
const [owner, ref] = rawRef
1994-
if (__DEV__ && !owner) {
1995-
warn(
1996-
`Missing ref owner context. ref cannot be used on hoisted vnodes. ` +
1997-
`A vnode with ref must be created inside the render function.`
1998-
)
1999-
return
2000-
}
2001-
const oldRef = oldRawRef && oldRawRef[1]
2002-
const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
2003-
const setupState = owner.setupState
2004-
2005-
// unset old ref
2006-
if (oldRef != null && oldRef !== ref) {
2007-
if (isString(oldRef)) {
2008-
refs[oldRef] = null
2009-
if (hasOwn(setupState, oldRef)) {
2010-
setupState[oldRef] = null
2011-
}
2012-
} else if (isRef(oldRef)) {
2013-
oldRef.value = null
2014-
}
2015-
}
2016-
2017-
if (isString(ref)) {
2018-
refs[ref] = value
2019-
if (hasOwn(setupState, ref)) {
2020-
setupState[ref] = value
2021-
}
2022-
} else if (isRef(ref)) {
2023-
ref.value = value
2024-
} else if (isFunction(ref)) {
2025-
callWithErrorHandling(ref, parent, ErrorCodes.FUNCTION_REF, [value, refs])
2026-
} else if (__DEV__) {
2027-
warn('Invalid template ref type:', value, `(${typeof value})`)
2028-
}
2029-
}
2030-
20312033
/**
20322034
* #1156
20332035
* When a component is HMR-enabled, we need to make sure that all static nodes

0 commit comments

Comments
 (0)