diff --git a/packages/runtime-core/__tests__/componentProps.spec.ts b/packages/runtime-core/__tests__/componentProps.spec.ts
index a97c9d481..658ef5c56 100644
--- a/packages/runtime-core/__tests__/componentProps.spec.ts
+++ b/packages/runtime-core/__tests__/componentProps.spec.ts
@@ -11,7 +11,8 @@ import {
createApp,
provide,
inject,
- watch
+ watch,
+ toRefs
} from '@vue/runtime-test'
import { render as domRender, nextTick } from 'vue'
@@ -479,4 +480,32 @@ describe('component props', () => {
expect(serializeInner(root)).toMatch(`
11
`)
expect(count).toBe(0)
})
+
+ // #3288
+ test('declared prop key should be present even if not passed', async () => {
+ let initialKeys: string[] = []
+ const changeSpy = jest.fn()
+ const passFoo = ref(false)
+
+ const Comp = {
+ render() {},
+ props: {
+ foo: String
+ },
+ setup(props: any) {
+ initialKeys = Object.keys(props)
+ const { foo } = toRefs(props)
+ watch(foo, changeSpy)
+ }
+ }
+
+ const Parent = () => (passFoo.value ? h(Comp, { foo: 'ok' }) : h(Comp))
+ const root = nodeOps.createElement('div')
+ createApp(Parent).mount(root)
+
+ expect(initialKeys).toMatchObject(['foo'])
+ passFoo.value = true
+ await nextTick()
+ expect(changeSpy).toHaveBeenCalledTimes(1)
+ })
})
diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts
index 476fba887..0238bf80e 100644
--- a/packages/runtime-core/src/componentProps.ts
+++ b/packages/runtime-core/src/componentProps.ts
@@ -143,6 +143,14 @@ export function initProps(
instance.propsDefaults = Object.create(null)
setFullProps(instance, rawProps, props, attrs)
+
+ // ensure all declared prop keys are present
+ for (const key in instance.propsOptions[0]) {
+ if (!(key in props)) {
+ props[key] = undefined
+ }
+ }
+
// validation
if (__DEV__) {
validateProps(rawProps || {}, props, instance)
@@ -281,11 +289,11 @@ function setFullProps(
const [options, needCastKeys] = instance.propsOptions
if (rawProps) {
for (const key in rawProps) {
- const value = rawProps[key]
// key, ref are reserved and never passed down
if (isReservedProp(key)) {
continue
}
+ const value = rawProps[key]
// prop option names are camelized during normalization, so to support
// kebab -> camel conversion here we need to camelize the key.
let camelKey