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) diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index d5c97a52b83..87dfcc1cc27 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -16,19 +16,12 @@ 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. + // 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 parentProvides = - currentInstance.parent && currentInstance.parent.provides - if (parentProvides === provides) { - provides = currentInstance.provides = Object.create(parentProvides) - } // TS doesn't allow symbol as index type - provides[key as string] = value + currentInstance.provides[key as string] = value } } diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index d51bbe1d2f5..220b5540d23 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -633,7 +633,10 @@ export function createComponentInstance( exposeProxy: null, withProxy: null, - provides: parent ? parent.provides : Object.create(appContext.provides), + // 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), ids: parent ? parent.ids : ['', 0, 0], accessCache: null!, renderCache: [],