From 6e50798139317154cc8f8755345d6ccd0f500333 Mon Sep 17 00:00:00 2001 From: Benjamin <33324686+benjamin-larsen@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:57:23 +0200 Subject: [PATCH 1/6] Dependency Injection Inheritance: Solution A --- packages/runtime-core/src/apiInject.ts | 12 +----------- packages/runtime-core/src/component.ts | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index d5c97a52b83..90285291d04 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -16,17 +16,7 @@ export function provide | string | number>( warn(`provide() can only be used inside setup().`) } } else { - let provides = currentInstance.provides - // by default an instance inherits its parent's provides object - // but when it needs to provide values of its own, it creates its - // own provides object using parent provides object as prototype. - // this way in `inject` we can simply look up injections from direct - // parent and let the prototype chain do the work. - const parentProvides = - currentInstance.parent && currentInstance.parent.provides - if (parentProvides === provides) { - provides = currentInstance.provides = Object.create(parentProvides) - } + const provides = currentInstance.provides // TS doesn't allow symbol as index type provides[key as string] = value } diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 9eb9193da07..ce6ad975c18 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -633,7 +633,7 @@ export function createComponentInstance( exposeProxy: null, withProxy: null, - provides: parent ? parent.provides : Object.create(appContext.provides), + provides: parent ? Object.create(parent.provides) : Object.create(appContext.provides), ids: parent ? parent.ids : ['', 0, 0], accessCache: null!, renderCache: [], From 707a9c7184ccb623ea849eabf0469e63c7a8bba6 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:50:30 +0000 Subject: [PATCH 2/6] [autofix.ci] apply automated fixes --- packages/runtime-core/src/component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index ce6ad975c18..950df802bd7 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -633,7 +633,9 @@ export function createComponentInstance( exposeProxy: null, withProxy: null, - provides: parent ? Object.create(parent.provides) : Object.create(appContext.provides), + provides: parent + ? Object.create(parent.provides) + : Object.create(appContext.provides), ids: parent ? parent.ids : ['', 0, 0], accessCache: null!, renderCache: [], From 64baaf64c9c360376f912e687806e62b94b6ec31 Mon Sep 17 00:00:00 2001 From: Benjamin <33324686+benjamin-larsen@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:39:37 +0200 Subject: [PATCH 3/6] test(apiInject): add unit tests for Dependency Injection Inheritance bug --- .../runtime-core/__tests__/apiInject.spec.ts | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/packages/runtime-core/__tests__/apiInject.spec.ts b/packages/runtime-core/__tests__/apiInject.spec.ts index e5c9267e5bb..71d6b16d10e 100644 --- a/packages/runtime-core/__tests__/apiInject.spec.ts +++ b/packages/runtime-core/__tests__/apiInject.spec.ts @@ -6,6 +6,8 @@ import { hasInjectionContext, inject, nextTick, + onBeforeUpdate, + onMounted, provide, reactive, readonly, @@ -347,6 +349,104 @@ describe('api: provide/inject', () => { expect(serialize(root)).toBe(`
`) }) + // #13921 + it('overlapping inheritance cycles', async () => { + let shouldProvide = ref(false) + + const Comp4 = { + props: ['data'], + setup() { + const data = ref('foo -1') + + onMounted(() => { + data.value = inject('foo', 'foo 0') + }) + + onBeforeUpdate(() => { + data.value = inject('foo', 'foo 0') + }) + + return () => [h('div', data.value)] + }, + } + + const Comp3 = { + props: ['data'], + setup() { + const data = ref('foo -1') + + onMounted(() => { + data.value = inject('foo', 'foo 0') + }) + + onBeforeUpdate(() => { + data.value = inject('foo', 'foo 0') + }) + + return () => [ + h('div', data.value), + h(Comp4, { data: shouldProvide.value }), + ] + }, + } + + const Comp2 = { + setup() { + const data = ref('foo -1') + + onMounted(() => { + data.value = inject('foo', 'foo 0') + }) + + onBeforeUpdate(() => { + if (shouldProvide.value) { + provide('foo', 'foo 2') + } + + data.value = inject('foo', 'foo 0') + }) + + return () => [ + h('div', data.value), + h(Comp3, { data: shouldProvide.value }), + ] + }, + } + + const Comp1 = { + setup() { + provide('foo', 'foo 1') + const data = ref('foo -1') + + onMounted(() => { + data.value = inject('foo', 'foo 0') + }) + + onBeforeUpdate(() => { + data.value = inject('foo', 'foo 0') + }) + + return () => [h('div', data.value), h(Comp2)] + }, + } + + const root = nodeOps.createElement('div') + render(h(Comp1), root) + + shouldProvide.value = true + await nextTick() + + /* + First (Root Component) should be "foo 0" because it is the Root Component and provdes shall only be injected to Descandents. + Second (Component 2) should be "foo 1" because it should inherit the provide from the Root Component + Third (Component 3) should be "foo 2" because it should inherit the provide from Component 2 (in the second render when shouldProvide = true) + Fourth (Component 4) should also be "foo 2" because it should inherit the provide from Component 3 which should inherit it from Component 2 (in the second render when shouldProvide = true) + */ + expect(serialize(root)).toBe( + `
foo 0
foo 1
foo 2
foo 2
`, + ) + }) + describe('hasInjectionContext', () => { it('should be false outside of setup', () => { expect(hasInjectionContext()).toBe(false) From d3bd9f48d3bbc26d082e49ed7245401761282034 Mon Sep 17 00:00:00 2001 From: Benjamin <33324686+benjamin-larsen@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:28:17 +0200 Subject: [PATCH 4/6] docs(apiInject): re-add modified documentation in provide() --- packages/runtime-core/src/apiInject.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index 90285291d04..608d342eb47 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -16,6 +16,10 @@ export function provide | string | number>( warn(`provide() can only be used inside setup().`) } } else { + // by default an instance it creates its own provides object + // using parent provides object as prototype. + // this way in `inject` we can simply look up injections from direct + // parent and let the prototype chain do the work. const provides = currentInstance.provides // TS doesn't allow symbol as index type provides[key as string] = value From 74610a520aeaa9766682cdb4430213ffea965a61 Mon Sep 17 00:00:00 2001 From: Benjamin Larsen <33324686+benjamin-larsen@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:23:20 +0200 Subject: [PATCH 5/6] chore(apiInject): remove constant provides and directly change instance.provides --- packages/runtime-core/src/apiInject.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index 608d342eb47..87dfcc1cc27 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -20,9 +20,8 @@ export function provide | string | number>( // using parent provides object as prototype. // this way in `inject` we can simply look up injections from direct // parent and let the prototype chain do the work. - const provides = currentInstance.provides // TS doesn't allow symbol as index type - provides[key as string] = value + currentInstance.provides[key as string] = value } } From 3662e10b6ac4fcae4c67b8392d31caa071dcb366 Mon Sep 17 00:00:00 2001 From: Benjamin Larsen <33324686+benjamin-larsen@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:28:22 +0200 Subject: [PATCH 6/6] docs(component): comment on provides --- packages/runtime-core/src/component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 55c45a4c451..220b5540d23 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -633,6 +633,7 @@ export function createComponentInstance( exposeProxy: null, withProxy: null, + // component instance always creates a new Provides object with prototype of parent provides (or app/global provides when there is no parent instance) so to ensure that Parent provides will always be inherited, even when parent provides after the fact. provides: parent ? Object.create(parent.provides) : Object.create(appContext.provides),