diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 243bde548c5..9d97bb18593 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -562,3 +562,7 @@ export { initFeatureFlags } from './featureFlags' * @internal */ export { createInternalObject } from './internalObject' +/** + * @internal + */ +export { createCanSetSetupRefChecker } from './rendererTemplateRef' diff --git a/packages/runtime-core/src/rendererTemplateRef.ts b/packages/runtime-core/src/rendererTemplateRef.ts index 31fcf8c2d5b..84f0bda1b95 100644 --- a/packages/runtime-core/src/rendererTemplateRef.ts +++ b/packages/runtime-core/src/rendererTemplateRef.ts @@ -14,7 +14,11 @@ import { warn } from './warning' import { isRef, toRaw } from '@vue/reactivity' import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { queuePostRenderEffect } from './renderer' -import { type ComponentOptions, getComponentPublicInstance } from './component' +import { + type ComponentOptions, + type Data, + getComponentPublicInstance, +} from './component' import { knownTemplateRefs } from './helpers/useTemplateRef' /** @@ -73,25 +77,7 @@ export function setRef( const oldRef = oldRawRef && (oldRawRef as VNodeNormalizedRefAtom).r const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs const setupState = owner.setupState - const rawSetupState = toRaw(setupState) - const canSetSetupRef = - setupState === EMPTY_OBJ - ? () => false - : (key: string) => { - if (__DEV__) { - if (hasOwn(rawSetupState, key) && !isRef(rawSetupState[key])) { - warn( - `Template ref "${key}" used on a non-ref value. ` + - `It will not work in the production build.`, - ) - } - - if (knownTemplateRefs.has(rawSetupState[key] as any)) { - return false - } - } - return hasOwn(rawSetupState, key) - } + const canSetSetupRef = createCanSetSetupRefChecker(setupState) // dynamic ref changed. unset old ref if (oldRef != null && oldRef !== ref) { @@ -161,3 +147,26 @@ export function setRef( } } } + +export function createCanSetSetupRefChecker( + setupState: Data, +): (key: string) => boolean { + const rawSetupState = toRaw(setupState) + return setupState === EMPTY_OBJ + ? () => false + : (key: string) => { + if (__DEV__) { + if (hasOwn(rawSetupState, key) && !isRef(rawSetupState[key])) { + warn( + `Template ref "${key}" used on a non-ref value. ` + + `It will not work in the production build.`, + ) + } + + if (knownTemplateRefs.has(rawSetupState[key] as any)) { + return false + } + } + return hasOwn(rawSetupState, key) + } +} diff --git a/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts b/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts index f1ce23ac156..4e8a1a9253c 100644 --- a/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts +++ b/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts @@ -19,6 +19,7 @@ import { nextTick, reactive, ref, + shallowRef, useTemplateRef, watchEffect, } from '@vue/runtime-dom' @@ -208,8 +209,8 @@ describe('api: template ref', () => { const { render } = define({ setup() { return { - foo: fooEl, - bar: barEl, + foo: shallowRef(fooEl), + bar: shallowRef(barEl), } }, render() { @@ -251,6 +252,7 @@ describe('api: template ref', () => { }) const { host } = render() expect(state.refKey).toBe(host.children[0]) + expect('Template ref "refKey" used on a non-ref value').toHaveBeenWarned() }) test('multiple root refs', () => { @@ -713,6 +715,45 @@ describe('api: template ref', () => { expect(html()).toBe('
changed
') }) + test('should not attempt to set when variable name is same as key', () => { + let tRef: ShallowRef + const key = 'refKey' + define({ + setup() { + tRef = useTemplateRef('_') + return { + [key]: tRef, + } + }, + render() { + const n0 = template('
')() as Element + createTemplateRefSetter()(n0, key) + return n0 + }, + }).render() + expect('target is readonly').not.toHaveBeenWarned() + expect(tRef!.value).toBe(null) + }) + + test('should work when used as direct ref value (compiled in prod mode)', () => { + __DEV__ = false + try { + let foo: ShallowRef + const { host } = define({ + setup() { + foo = useTemplateRef('foo') + const n0 = template('
')() as Element + createTemplateRefSetter()(n0, foo) + return n0 + }, + }).render() + expect('target is readonly').not.toHaveBeenWarned() + expect(foo!.value).toBe(host.children[0]) + } finally { + __DEV__ = true + } + }) + // TODO: can not reproduce in Vapor // // #2078 // test('handling multiple merged refs', async () => { diff --git a/packages/runtime-vapor/src/apiTemplateRef.ts b/packages/runtime-vapor/src/apiTemplateRef.ts index 7a30d219811..a14f373e7de 100644 --- a/packages/runtime-vapor/src/apiTemplateRef.ts +++ b/packages/runtime-vapor/src/apiTemplateRef.ts @@ -9,6 +9,7 @@ import { ErrorCodes, type SchedulerJob, callWithErrorHandling, + createCanSetSetupRefChecker, queuePostFlushCb, warn, } from '@vue/runtime-dom' @@ -55,6 +56,7 @@ export function setRef( const refs = instance.refs === EMPTY_OBJ ? (instance.refs = {}) : instance.refs + const canSetSetupRef = createCanSetSetupRefChecker(setupState) // dynamic ref changed. unset old ref if (oldRef != null && oldRef !== ref) { if (isString(oldRef)) { @@ -87,7 +89,7 @@ export function setRef( const doSet: SchedulerJob = () => { if (refFor) { existing = _isString - ? __DEV__ && hasOwn(setupState, ref) + ? __DEV__ && canSetSetupRef(ref) ? setupState[ref] : refs[ref] : ref.value @@ -96,7 +98,7 @@ export function setRef( existing = [refValue] if (_isString) { refs[ref] = existing - if (__DEV__ && hasOwn(setupState, ref)) { + if (__DEV__ && canSetSetupRef(ref)) { setupState[ref] = refs[ref] // if setupState[ref] is a reactivity ref, // the existing will also become reactivity too @@ -111,7 +113,7 @@ export function setRef( } } else if (_isString) { refs[ref] = refValue - if (__DEV__ && hasOwn(setupState, ref)) { + if (__DEV__ && canSetSetupRef(ref)) { setupState[ref] = refValue } } else if (_isRef) { @@ -129,7 +131,7 @@ export function setRef( remove(existing, refValue) } else if (_isString) { refs[ref] = null - if (__DEV__ && hasOwn(setupState, ref)) { + if (__DEV__ && canSetSetupRef(ref)) { setupState[ref] = null } } else if (_isRef) {