Skip to content

Commit 4a2953f

Browse files
authored
fix(runtime-core): avoid setting direct ref of useTemplateRef in dev (#13449)
close 12852
1 parent 19a0cbd commit 4a2953f

File tree

2 files changed

+217
-6
lines changed

2 files changed

+217
-6
lines changed

packages/runtime-core/__tests__/helpers/useTemplateRef.spec.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,134 @@ describe('useTemplateRef', () => {
106106
expect(tRef!.value).toBe(null)
107107
})
108108

109+
test('should work when used with direct ref value with ref_key', () => {
110+
let tRef: ShallowRef
111+
const key = 'refKey'
112+
const Comp = {
113+
setup() {
114+
tRef = useTemplateRef(key)
115+
return () => h('div', { ref: tRef, ref_key: key })
116+
},
117+
}
118+
const root = nodeOps.createElement('div')
119+
render(h(Comp), root)
120+
121+
expect('target is readonly').not.toHaveBeenWarned()
122+
expect(tRef!.value).toBe(root.children[0])
123+
})
124+
125+
test('should work when used with direct ref value with ref_key and ref_for', () => {
126+
let tRef: ShallowRef
127+
const key = 'refKey'
128+
const Comp = {
129+
setup() {
130+
tRef = useTemplateRef(key)
131+
},
132+
render() {
133+
return h(
134+
'div',
135+
[1, 2, 3].map(x =>
136+
h('span', { ref: tRef, ref_key: key, ref_for: true }, x.toString()),
137+
),
138+
)
139+
},
140+
}
141+
const root = nodeOps.createElement('div')
142+
render(h(Comp), root)
143+
144+
expect('target is readonly').not.toHaveBeenWarned()
145+
expect(tRef!.value).toHaveLength(3)
146+
})
147+
148+
test('should work when used with direct ref value with ref_key and dynamic value', async () => {
149+
const refMode = ref('h1-ref')
150+
151+
let tRef: ShallowRef
152+
const key = 'refKey'
153+
154+
const Comp = {
155+
setup() {
156+
tRef = useTemplateRef(key)
157+
},
158+
render() {
159+
switch (refMode.value) {
160+
case 'h1-ref':
161+
return h('h1', { ref: tRef, ref_key: key })
162+
case 'h2-ref':
163+
return h('h2', { ref: tRef, ref_key: key })
164+
case 'no-ref':
165+
return h('span')
166+
case 'nothing':
167+
return null
168+
}
169+
},
170+
}
171+
172+
const root = nodeOps.createElement('div')
173+
render(h(Comp), root)
174+
175+
expect(tRef!.value.tag).toBe('h1')
176+
177+
refMode.value = 'h2-ref'
178+
await nextTick()
179+
expect(tRef!.value.tag).toBe('h2')
180+
181+
refMode.value = 'no-ref'
182+
await nextTick()
183+
expect(tRef!.value).toBeNull()
184+
185+
refMode.value = 'nothing'
186+
await nextTick()
187+
expect(tRef!.value).toBeNull()
188+
189+
expect('target is readonly').not.toHaveBeenWarned()
190+
})
191+
192+
test('should work when used with dynamic direct refs and ref_keys', async () => {
193+
const refKey = ref('foo')
194+
195+
let tRefs: Record<string, ShallowRef>
196+
197+
const Comp = {
198+
setup() {
199+
tRefs = {
200+
foo: useTemplateRef('foo'),
201+
bar: useTemplateRef('bar'),
202+
}
203+
},
204+
render() {
205+
return h('div', { ref: tRefs[refKey.value], ref_key: refKey.value })
206+
},
207+
}
208+
209+
const root = nodeOps.createElement('div')
210+
render(h(Comp), root)
211+
212+
expect(tRefs!['foo'].value).toBe(root.children[0])
213+
expect(tRefs!['bar'].value).toBeNull()
214+
215+
refKey.value = 'bar'
216+
await nextTick()
217+
expect(tRefs!['foo'].value).toBeNull()
218+
expect(tRefs!['bar'].value).toBe(root.children[0])
219+
220+
expect('target is readonly').not.toHaveBeenWarned()
221+
})
222+
223+
test('should not work when used with direct ref value without ref_key (in dev mode)', () => {
224+
let tRef: ShallowRef
225+
const Comp = {
226+
setup() {
227+
tRef = useTemplateRef('refKey')
228+
return () => h('div', { ref: tRef })
229+
},
230+
}
231+
const root = nodeOps.createElement('div')
232+
render(h(Comp), root)
233+
234+
expect(tRef!.value).toBeNull()
235+
})
236+
109237
test('should work when used as direct ref value (compiled in prod mode)', () => {
110238
__DEV__ = false
111239
try {
@@ -125,4 +253,65 @@ describe('useTemplateRef', () => {
125253
__DEV__ = true
126254
}
127255
})
256+
257+
test('should work when used as direct ref value with ref_key and ref_for (compiled in prod mode)', () => {
258+
__DEV__ = false
259+
try {
260+
let tRef: ShallowRef
261+
const key = 'refKey'
262+
const Comp = {
263+
setup() {
264+
tRef = useTemplateRef(key)
265+
},
266+
render() {
267+
return h(
268+
'div',
269+
[1, 2, 3].map(x =>
270+
h(
271+
'span',
272+
{ ref: tRef, ref_key: key, ref_for: true },
273+
x.toString(),
274+
),
275+
),
276+
)
277+
},
278+
}
279+
280+
const root = nodeOps.createElement('div')
281+
render(h(Comp), root)
282+
283+
expect('target is readonly').not.toHaveBeenWarned()
284+
expect(tRef!.value).toHaveLength(3)
285+
} finally {
286+
__DEV__ = true
287+
}
288+
})
289+
290+
test('should work when used as direct ref value with ref_for but without ref_key (compiled in prod mode)', () => {
291+
__DEV__ = false
292+
try {
293+
let tRef: ShallowRef
294+
const Comp = {
295+
setup() {
296+
tRef = useTemplateRef('refKey')
297+
},
298+
render() {
299+
return h(
300+
'div',
301+
[1, 2, 3].map(x =>
302+
h('span', { ref: tRef, ref_for: true }, x.toString()),
303+
),
304+
)
305+
},
306+
}
307+
308+
const root = nodeOps.createElement('div')
309+
render(h(Comp), root)
310+
311+
expect('target is readonly').not.toHaveBeenWarned()
312+
expect(tRef!.value).toHaveLength(3)
313+
} finally {
314+
__DEV__ = true
315+
}
316+
})
128317
})

packages/runtime-core/src/rendererTemplateRef.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import type { SuspenseBoundary } from './components/Suspense'
2-
import type { VNode, VNodeNormalizedRef, VNodeNormalizedRefAtom } from './vnode'
2+
import type {
3+
VNode,
4+
VNodeNormalizedRef,
5+
VNodeNormalizedRefAtom,
6+
VNodeRef,
7+
} from './vnode'
38
import {
49
EMPTY_OBJ,
510
NO,
@@ -95,6 +100,10 @@ export function setRef(
95100
return hasOwn(rawSetupState, key)
96101
}
97102

103+
const canSetRef = (ref: VNodeRef) => {
104+
return !__DEV__ || !knownTemplateRefs.has(ref as any)
105+
}
106+
98107
// dynamic ref changed. unset old ref
99108
if (oldRef != null && oldRef !== ref) {
100109
if (isString(oldRef)) {
@@ -103,7 +112,13 @@ export function setRef(
103112
setupState[oldRef] = null
104113
}
105114
} else if (isRef(oldRef)) {
106-
oldRef.value = null
115+
if (canSetRef(oldRef)) {
116+
oldRef.value = null
117+
}
118+
119+
// this type assertion is valid since `oldRef` has already been asserted to be non-null
120+
const oldRawRefAtom = oldRawRef as VNodeNormalizedRefAtom
121+
if (oldRawRefAtom.k) refs[oldRawRefAtom.k] = null
107122
}
108123
}
109124

@@ -120,7 +135,9 @@ export function setRef(
120135
? canSetSetupRef(ref)
121136
? setupState[ref]
122137
: refs[ref]
123-
: ref.value
138+
: canSetRef(ref) || !rawRef.k
139+
? ref.value
140+
: refs[rawRef.k]
124141
if (isUnmount) {
125142
isArray(existing) && remove(existing, refValue)
126143
} else {
@@ -131,8 +148,11 @@ export function setRef(
131148
setupState[ref] = refs[ref]
132149
}
133150
} else {
134-
ref.value = [refValue]
135-
if (rawRef.k) refs[rawRef.k] = ref.value
151+
const newVal = [refValue]
152+
if (canSetRef(ref)) {
153+
ref.value = newVal
154+
}
155+
if (rawRef.k) refs[rawRef.k] = newVal
136156
}
137157
} else if (!existing.includes(refValue)) {
138158
existing.push(refValue)
@@ -144,7 +164,9 @@ export function setRef(
144164
setupState[ref] = value
145165
}
146166
} else if (_isRef) {
147-
ref.value = value
167+
if (canSetRef(ref)) {
168+
ref.value = value
169+
}
148170
if (rawRef.k) refs[rawRef.k] = value
149171
} else if (__DEV__) {
150172
warn('Invalid template ref type:', ref, `(${typeof ref})`)

0 commit comments

Comments
 (0)