Skip to content

Conversation

benjamin-larsen
Copy link

@benjamin-larsen benjamin-larsen commented Sep 24, 2025

close #13921

This is a solution to the issue #13921.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed provide/inject inheritance so child components inherit via prototype, preventing child mutations from affecting ancestor components.
  • Refactor

    • Component instances now create their own provides object that prototypically inherits from the parent for isolated, predictable behavior; public APIs unchanged.
  • Tests

    • Added tests validating provide/inject propagation across nested components and lifecycle timing (mount/update).

Copy link

coderabbitai bot commented Sep 24, 2025

Walkthrough

Child instances now get a prototypal provides object at creation (Object.create(parent.provides)); provide() now directly writes to instance.provides[key] without creating an on-demand child provides object. Tests added for overlapping provide/inject lifecycles. No public APIs changed.

Changes

Cohort / File(s) Summary
Component provides initialization
packages/runtime-core/src/component.ts
Non-root instances initialize instance.provides = Object.create(parent.provides) (root uses Object.create(appContext.provides)), establishing prototype inheritance at instantiation so child mutations don't mutate parent.
Provide behavior (no on-demand shadowing)
packages/runtime-core/src/apiInject.ts
provide() now assigns directly to currentInstance.provides[key] = value and no longer creates a child provides object from the parent when first providing a value. Comments updated to reflect the new behavior.
Tests for overlapping provide/inject
packages/runtime-core/__tests__/apiInject.spec.ts
Added/updated tests exercising overlapping inheritance and dynamic provide calls inside lifecycle hooks (onMounted, onBeforeUpdate), verifying propagation across multiple render passes and overlapping provide scenarios.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant App as App (root)
    participant P as Parent
    participant C as Child
    participant Runtime as runtime-core
    participant Provide as provide()
    participant Inject as inject()

    Note over Runtime: component instance creation
    App->>Runtime: createComponentInstance(appContext)
    Runtime->>P: create parent instance
    Runtime->>C: create child instance
    Runtime->>C: C.provides = Object.create(P.provides)
    Note right of C #DFF0D8: prototype chain established at init

    Note over Provide: providing a value
    C->>Provide: provide(key, value)
    Provide->>C: C.provides[key] = value
    Note right of Provide #FFF3CD: no on-demand cloning/shadowing

    Note over Inject: resolution
    C->>Inject: inject(key)
    Inject->>C: read C.provides[key] (falls back via prototype chain)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I hop through scopes where hidden keys reside,
I stitch a thread along a prototype stride,
No sudden cloning, no ancestor surprise,
Each child keeps its whisper, parents stay wise,
A rabbit cheers — the provides align. 🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The title references dependency injection inheritance but uses a non-descriptive label “Solution A,” which does not clearly convey the specific fix or behavior change implemented. It mentions the general area of the change but fails to summarize the core update to the provide() prototype chain and inheritance propagation. Consider renaming the title to clearly describe the fix, for example “Fix provide() propagation to all nested descendants” or something similar that highlights the change to the provide/inject inheritance behavior.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues Check ✅ Passed The implementation adjusts the prototype chain for provides in both apiInject and createComponentInstance to ensure that a new provided value on Component 2 propagates through all deeper descendants as described in issue #13921, and the added tests verify this behavior under overlapping render scenarios.
Out of Scope Changes Check ✅ Passed All changes are confined to the provide/inject implementation and associated tests for the nested inheritance scenario in issue #13921, and there are no unrelated or extraneous modifications in this pull request.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 74610a5 and 3662e10.

📒 Files selected for processing (1)
  • packages/runtime-core/src/component.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/runtime-core/src/component.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules
  • GitHub Check: Header rules
  • GitHub Check: Pages changed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/runtime-core/src/component.ts (1)

636-636: Prototypal provides creation is correct; note perf trade‑off

Using Object.create(parent.provides) (or appContext.provides) eagerly guarantees safe isolation and clean inheritance for DI. This is the right pairing with the updated provide().

Trade‑off: every instance now allocates a provides object even if it never calls provide(). Likely negligible, but worth a quick perf sanity check on large trees.

If desired, I can help run or outline a micro-benchmark comparing before/after memory allocs and mount times on a large component graph.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b555f02 and 6e50798.

📒 Files selected for processing (2)
  • packages/runtime-core/src/apiInject.ts (1 hunks)
  • packages/runtime-core/src/component.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/runtime-core/src/apiInject.ts (1)
packages/runtime-core/src/component.ts (1)
  • currentInstance (711-711)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules
  • GitHub Check: Header rules
  • GitHub Check: Pages changed
🔇 Additional comments (1)
packages/runtime-core/src/apiInject.ts (1)

19-22: Aligns with upfront prototypal provides; avoids parent mutation

Writing directly to currentInstance.provides is correct now that each instance gets its own provides with a prototype chain. This removes copy‑on‑write complexity and prevents accidental parent mutation.

Please ensure tests cover:

  • Child overriding a parent’s provide (descendants see child’s value).
  • Root providing after app.provide (children see both via prototype).
  • Symbol keys.
  • Custom elements and nested createApp cases.

I can draft these tests if helpful.

Copy link

github-actions bot commented Sep 24, 2025

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 102 kB (-67 B) 38.6 kB (-19 B) 34.7 kB (-45 B)
vue.global.prod.js 160 kB (-67 B) 58.7 kB (-15 B) 52.2 kB (-6 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 46.7 kB (+15 B) 18.3 kB (+7 B) 16.7 kB (-1 B)
createApp 54.6 kB (-63 B) 21.3 kB (-9 B) 19.4 kB (-15 B)
createSSRApp 58.8 kB (-63 B) 23 kB (-16 B) 21 kB (-25 B)
defineCustomElement 59.9 kB (-63 B) 23 kB (-11 B) 20.9 kB (-9 B)
overall 68.8 kB (-63 B) 26.5 kB (-15 B) 24.1 kB (-28 B)

@edison1105 edison1105 added the 🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. label Sep 24, 2025
@edison1105
Copy link
Member

Could you please add a test?

@edison1105 edison1105 added the need test The PR has missing test cases. label Sep 24, 2025
Copy link

pkg-pr-new bot commented Sep 24, 2025

Open in StackBlitz

@vue/compiler-core

npm i https://pkg.pr.new/@vue/compiler-core@13924

@vue/compiler-dom

npm i https://pkg.pr.new/@vue/compiler-dom@13924

@vue/compiler-sfc

npm i https://pkg.pr.new/@vue/compiler-sfc@13924

@vue/compiler-ssr

npm i https://pkg.pr.new/@vue/compiler-ssr@13924

@vue/reactivity

npm i https://pkg.pr.new/@vue/reactivity@13924

@vue/runtime-core

npm i https://pkg.pr.new/@vue/runtime-core@13924

@vue/runtime-dom

npm i https://pkg.pr.new/@vue/runtime-dom@13924

@vue/server-renderer

npm i https://pkg.pr.new/@vue/server-renderer@13924

@vue/shared

npm i https://pkg.pr.new/@vue/shared@13924

vue

npm i https://pkg.pr.new/vue@13924

@vue/compat

npm i https://pkg.pr.new/@vue/compat@13924

commit: 3bf1673

@benjamin-larsen
Copy link
Author

@edison1105 I have now made the tests, I have tested the tests with the changes reverted (with it failing, like expected) and the the test passing with the changes in the PR (as expected).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
packages/runtime-core/__tests__/apiInject.spec.ts (4)

433-438: Add a tick after initial mount to avoid scheduler-order flakiness.

Without awaiting a tick after the initial render, mounted hooks and the subsequent update triggered by shouldProvide can interleave, making the assertion timing-sensitive. Insert an extra await nextTick() right after render(h(Comp1), root).

Apply this diff:

     const root = nodeOps.createElement('div')
     render(h(Comp1), root)
 
+    // ensure all mounted hooks have flushed before triggering the update
+    await nextTick()
+
     shouldProvide.value = true
     await nextTick()

354-354: Use const for the ref.

The variable isn’t reassigned; prefer const for clarity.

-    let shouldProvide = ref(false)
+    const shouldProvide = ref(false)

357-371: Avoid shadowing by renaming the prop from "data" to "trigger".

Each component declares a prop named data while also defining a local ref called data, which is confusing. Rename the prop to trigger and update call sites. This preserves the intent (force an update) and improves readability.

     const Comp4 = {
-      props: ['data'],
+      props: ['trigger'],
       setup() {
         const data = ref('foo -1')
@@
     const Comp3 = {
-      props: ['data'],
+      props: ['trigger'],
       setup() {
         const data = ref('foo -1')
@@
-        return () => [
-          h('div', data.value),
-          h(Comp4, { data: shouldProvide.value }),
-        ]
+        return () => [
+          h('div', data.value),
+          h(Comp4, { trigger: shouldProvide.value }),
+        ]
@@
-        return () => [
-          h('div', data.value),
-          h(Comp3, { data: shouldProvide.value }),
-        ]
+        return () => [
+          h('div', data.value),
+          h(Comp3, { trigger: shouldProvide.value }),
+        ]

Also applies to: 373-392, 386-389, 409-412


439-444: Polish the explanatory comment (optional).

Minor typos; consider tightening wording for future readers.

-      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)
+      First (root) should be "foo 0" (root cannot self-inject; falls back to default).
+      Second (Comp2) should be "foo 1" (inherits from root before updating its own provide).
+      Third (Comp3) should be "foo 2" (inherits Comp2’s updated provide during the second render).
+      Fourth (Comp4) should also be "foo 2" (inherits from Comp3, which inherits from Comp2).
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 707a9c7 and 64baaf6.

📒 Files selected for processing (1)
  • packages/runtime-core/__tests__/apiInject.spec.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/runtime-core/__tests__/apiInject.spec.ts (2)
packages/runtime-core/src/apiInject.ts (2)
  • inject (36-74)
  • provide (10-23)
packages/runtime-test/src/serialize.ts (1)
  • serialize (10-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules
  • GitHub Check: Header rules
  • GitHub Check: Pages changed
🔇 Additional comments (1)
packages/runtime-core/__tests__/apiInject.spec.ts (1)

9-10: LGTM: lifecycle imports align with the test scenario.

Importing onBeforeUpdate/onMounted is appropriate for validating DI updates across update cycles.


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.

}
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

@edison1105 edison1105 added wait changes and removed need test The PR has missing test cases. labels Sep 25, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bf1673 and 74610a5.

📒 Files selected for processing (1)
  • packages/runtime-core/src/apiInject.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/runtime-core/src/apiInject.ts (1)
packages/runtime-core/src/component.ts (1)
  • currentInstance (713-713)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules
  • GitHub Check: Header rules
  • GitHub Check: Pages changed

Comment on lines +19 to +24
// 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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Restore the per-instance provides fork before writing.

currentInstance.provides still aliases parent.provides until we fork it (see createComponentInstance in component.ts). With this change we now write straight into the parent’s map, so a child re-provide will bleed into siblings and ancestors. We need to restore the Object.create(parentProvides) guard (or otherwise ensure each instance owns its own map) before assigning the new value.

-    // 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.
-    // TS doesn't allow symbol as index type
-    currentInstance.provides[key as string] = value
+    let { provides, parent } = currentInstance
+    if (parent) {
+      const parentProvides = parent.provides
+      if (provides === parentProvides) {
+        provides = currentInstance.provides = Object.create(parentProvides)
+      }
+    }
+    // TS doesn't allow symbol as index type
+    provides[key as string] = value
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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
let { provides, parent } = currentInstance
if (parent) {
const parentProvides = parent.provides
if (provides === parentProvides) {
provides = currentInstance.provides = Object.create(parentProvides)
}
}
// TS doesn't allow symbol as index type
provides[key as string] = value
🤖 Prompt for AI Agents
In packages/runtime-core/src/apiInject.ts around lines 19 to 24, the code writes
directly into currentInstance.provides which still aliases parent.provides;
restore the per-instance provides fork before assigning so we don't mutate the
parent's map. Detect when currentInstance.provides === parent.provides (or when
it shares reference) and replace it with a shallow clone created from the parent
(e.g., Object.create(parentProvides)) so the instance owns its own map, then
assign the key (converting symbol keys to string as needed) into that
per-instance provides object.

@benjamin-larsen
Copy link
Author

I have now added your suggestions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. wait changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Dependency Injection inheritance fails when provided after grandchildren are rendered.
2 participants