diff --git a/packages/runtime-core/__tests__/rendererComponent.spec.ts b/packages/runtime-core/__tests__/rendererComponent.spec.ts
index 02070d384e9..85ead165402 100644
--- a/packages/runtime-core/__tests__/rendererComponent.spec.ts
+++ b/packages/runtime-core/__tests__/rendererComponent.spec.ts
@@ -296,4 +296,51 @@ describe('renderer: component', () => {
expect(serializeInner(root)).toBe(`
1
`)
})
})
+
+ // #3371
+ test(`should not cause an infinite loop when the child component's props track the parent component's render fn`, 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 a72356184a0..53faaf5851a 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
+ const rawCurrentProps = extend({}, toRaw(props))
if (rawProps) {
for (let key in rawProps) {
// key, ref are reserved and never passed down
@@ -337,7 +338,7 @@ function setFullProps(
// kebab -> camel conversion here we need to camelize the key.
let camelKey
if (options && hasOwn(options, (camelKey = camelize(key)))) {
- props[camelKey] = value
+ rawCurrentProps[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,7 +359,6 @@ function setFullProps(
}
if (needCastKeys) {
- const rawCurrentProps = toRaw(props)
for (let i = 0; i < needCastKeys.length; i++) {
const key = needCastKeys[i]
props[key] = resolvePropValue(
@@ -370,7 +370,12 @@ function setFullProps(
)
}
}
-
+ if (options) {
+ for (const key in rawCurrentProps) {
+ if (!needCastKeys || !needCastKeys.includes(key))
+ props[key] = rawCurrentProps[key]
+ }
+ }
return hasAttrsChanged
}