diff --git a/packages/runtime-core/__tests__/componentProps.spec.ts b/packages/runtime-core/__tests__/componentProps.spec.ts index 658ef5c567a..ac1dc13dfcc 100644 --- a/packages/runtime-core/__tests__/componentProps.spec.ts +++ b/packages/runtime-core/__tests__/componentProps.spec.ts @@ -12,7 +12,8 @@ import { provide, inject, watch, - toRefs + toRefs, + SetupContext } from '@vue/runtime-test' import { render as domRender, nextTick } from 'vue' @@ -508,4 +509,51 @@ describe('component props', () => { await nextTick() expect(changeSpy).toHaveBeenCalledTimes(1) }) + + // #3371 + test(`avoid double-setting props when casting`, async () => { + const Parent = { + setup(props: any, { slots }: SetupContext) { + const childProps = ref() + const registerChildProps = (props: any) => { + childProps.value = props + } + provide('register', registerChildProps) + + return () => { + // access the child component's props + childProps.value && childProps.value.foo + return slots.default!() + } + } + } + + const Child = { + props: { + foo: { + type: Boolean, + required: false + } + }, + setup(props: { foo: boolean }) { + const register = inject('register') as any + // 1. change the reactivity data of the parent component + // 2. register its own props to the parent component + register(props) + + return () => 'foo' + } + } + + const App = { + setup() { + return () => h(Parent, () => h(Child as any, { foo: '' }, () => null)) + } + } + + const root = nodeOps.createElement('div') + render(h(App), root) + await nextTick() + expect(serializeInner(root)).toBe(`foo`) + }) }) diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index d86eb969894..f94d1522ccd 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -312,6 +312,7 @@ function setFullProps( ) { const [options, needCastKeys] = instance.propsOptions let hasAttrsChanged = false + let rawCastValues: Data | undefined if (rawProps) { for (let key in rawProps) { // key, ref are reserved and never passed down @@ -337,7 +338,11 @@ function setFullProps( // kebab -> camel conversion here we need to camelize the key. let camelKey if (options && hasOwn(options, (camelKey = camelize(key)))) { - props[camelKey] = value + if (!needCastKeys || !needCastKeys.includes(camelKey)) { + props[camelKey] = value + } else { + ;(rawCastValues || (rawCastValues = {}))[camelKey] = value + } } else if (!isEmitListener(instance.emitsOptions, key)) { // Any non-declared (either as a prop or an emitted event) props are put // into a separate `attrs` object for spreading. Make sure to preserve @@ -358,14 +363,13 @@ function setFullProps( } if (needCastKeys) { - const rawCurrentProps = toRaw(props) for (let i = 0; i < needCastKeys.length; i++) { const key = needCastKeys[i] props[key] = resolvePropValue( options!, - rawCurrentProps, + rawCastValues || EMPTY_OBJ, key, - rawCurrentProps[key], + rawCastValues && rawCastValues[key], instance ) } @@ -408,13 +412,13 @@ function resolvePropValue( } // boolean casting if (opt[BooleanFlags.shouldCast]) { - if (!hasOwn(props, key) && !hasDefault) { - value = false - } else if ( + if ( opt[BooleanFlags.shouldCastTrue] && (value === '' || value === hyphenate(key)) ) { value = true + } else if (!hasOwn(props, key) && !hasDefault) { + value = false } } }