Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions packages/runtime-core/__tests__/apiInject.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
hasInjectionContext,
inject,
nextTick,
onBeforeUpdate,
onMounted,
provide,
reactive,
readonly,
Expand Down Expand Up @@ -347,6 +349,104 @@ describe('api: provide/inject', () => {
expect(serialize(root)).toBe(`<div><!----></div>`)
})

// #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(
`<div><div>foo 0</div><div>foo 1</div><div>foo 2</div><div>foo 2</div></div>`,
)
})

describe('hasInjectionContext', () => {
it('should be false outside of setup', () => {
expect(hasInjectionContext()).toBe(false)
Expand Down
12 changes: 1 addition & 11 deletions packages/runtime-core/src/apiInject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,7 @@ export function provide<T, K = InjectionKey<T> | 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
provides[key as string] = value
currentInstance.provides.provides[key as string] = value

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will integrate this into a commit, however provides const should be removed, and it would be currentInstance.provides not currentInstance.provides.provides

}
Expand Down
4 changes: 3 additions & 1 deletion packages/runtime-core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,9 @@ export function createComponentInstance(
exposeProxy: null,
withProxy: null,

provides: parent ? parent.provides : Object.create(appContext.provides),
provides: parent
? Object.create(parent.provides)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above comment should be added here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original comment was in apiInject.ts, but I can copy or move it to there too.

: Object.create(appContext.provides),
ids: parent ? parent.ids : ['', 0, 0],
accessCache: null!,
renderCache: [],
Expand Down