diff --git a/packages/runtime-core/__tests__/hmr.spec.ts b/packages/runtime-core/__tests__/hmr.spec.ts
index d1392b78465..b81a8b3af63 100644
--- a/packages/runtime-core/__tests__/hmr.spec.ts
+++ b/packages/runtime-core/__tests__/hmr.spec.ts
@@ -537,4 +537,35 @@ describe('hot module replacement', () => {
render(h(Foo), root)
expect(serializeInner(root)).toBe('bar')
})
+
+ // #7155 - force HMR on slots content update
+ test('force update slot content change', () => {
+ const root = nodeOps.createElement('div')
+ const parentId = 'test-force-computed-parent'
+ const childId = 'test-force-computed-child'
+
+ const Child: ComponentOptions = {
+ __hmrId: childId,
+ computed: {
+ slotContent() {
+ return this.$slots.default?.()
+ }
+ },
+ render: compileToFunction(``)
+ }
+ createRecord(childId, Child)
+
+ const Parent: ComponentOptions = {
+ __hmrId: parentId,
+ components: { Child },
+ render: compileToFunction(`1`)
+ }
+ createRecord(parentId, Parent)
+
+ render(h(Parent), root)
+ expect(serializeInner(root)).toBe(`1`)
+
+ rerender(parentId, compileToFunction(`2`))
+ expect(serializeInner(root)).toBe(`2`)
+ })
})
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 941231b393d..087e901354b 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -349,6 +349,10 @@ export interface ComponentInternalInstance {
slots: InternalSlots
refs: Data
emit: EmitFn
+
+ attrsProxy: Data | null
+ slotsProxy: Slots | null
+
/**
* used for keeping track of .once event handlers on components
* @internal
@@ -536,6 +540,9 @@ export function createComponentInstance(
setupState: EMPTY_OBJ,
setupContext: null,
+ attrsProxy: null,
+ slotsProxy: null,
+
// suspense related
suspense,
suspenseId: suspense ? suspense.pendingId : 0,
@@ -923,31 +930,57 @@ export function finishComponentSetup(
}
}
-function createAttrsProxy(instance: ComponentInternalInstance): Data {
- return new Proxy(
- instance.attrs,
- __DEV__
- ? {
- get(target, key: string) {
- markAttrsAccessed()
- track(instance, TrackOpTypes.GET, '$attrs')
- return target[key]
- },
- set() {
- warn(`setupContext.attrs is readonly.`)
- return false
- },
- deleteProperty() {
- warn(`setupContext.attrs is readonly.`)
- return false
+function getAttrsProxy(instance: ComponentInternalInstance): Data {
+ return (
+ instance.attrsProxy ||
+ (instance.attrsProxy = new Proxy(
+ instance.attrs,
+ __DEV__
+ ? {
+ get(target, key: string) {
+ markAttrsAccessed()
+ track(instance, TrackOpTypes.GET, '$attrs')
+ return target[key]
+ },
+ set() {
+ warn(`setupContext.attrs is readonly.`)
+ return false
+ },
+ deleteProperty() {
+ warn(`setupContext.attrs is readonly.`)
+ return false
+ }
}
- }
- : {
- get(target, key: string) {
- track(instance, TrackOpTypes.GET, '$attrs')
- return target[key]
+ : {
+ get(target, key: string) {
+ track(instance, TrackOpTypes.GET, '$attrs')
+ return target[key]
+ }
}
- }
+ ))
+ )
+}
+
+/**
+ * Dev-only
+ */
+function getSlotsProxy(instance: ComponentInternalInstance): Slots {
+ return (
+ instance.slotsProxy ||
+ (instance.slotsProxy = new Proxy(instance.slots, {
+ get(target, key: string) {
+ track(instance, TrackOpTypes.GET, '$slots')
+ return target[key]
+ },
+ set() {
+ warn(`setupContext.slots is readonly.`)
+ return false
+ },
+ deleteProperty() {
+ warn(`setupContext.slots is readonly.`)
+ return false
+ }
+ }))
)
}
@@ -978,16 +1011,15 @@ export function createSetupContext(
instance.exposed = exposed || {}
}
- let attrs: Data
if (__DEV__) {
// We use getters in dev in case libs like test-utils overwrite instance
// properties (overwrites should not be done in prod)
return Object.freeze({
get attrs() {
- return attrs || (attrs = createAttrsProxy(instance))
+ return getAttrsProxy(instance)
},
get slots() {
- return shallowReadonly(instance.slots)
+ return getSlotsProxy(instance)
},
get emit() {
return (event: string, ...args: any[]) => instance.emit(event, ...args)
@@ -997,7 +1029,7 @@ export function createSetupContext(
} else {
return {
get attrs() {
- return attrs || (attrs = createAttrsProxy(instance))
+ return getAttrsProxy(instance)
},
slots: instance.slots,
emit: instance.emit,
diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts
index 7b0ccf77ac9..dd2d29670e6 100644
--- a/packages/runtime-core/src/componentPublicInstance.ts
+++ b/packages/runtime-core/src/componentPublicInstance.ts
@@ -356,6 +356,8 @@ export const PublicInstanceProxyHandlers: ProxyHandler = {
if (key === '$attrs') {
track(instance, TrackOpTypes.GET, key)
__DEV__ && markAttrsAccessed()
+ } else if (__DEV__ && key === '$slots') {
+ track(instance, TrackOpTypes.GET, key)
}
return publicGetter(instance)
} else if (
diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts
index 81988599981..8f59099d833 100644
--- a/packages/runtime-core/src/componentSlots.ts
+++ b/packages/runtime-core/src/componentSlots.ts
@@ -23,6 +23,8 @@ import { ContextualRenderFn, withCtx } from './componentRenderContext'
import { isHmrUpdating } from './hmr'
import { DeprecationTypes, isCompatEnabled } from './compat/compatConfig'
import { toRaw } from '@vue/reactivity'
+import { trigger } from '@vue/reactivity'
+import { TriggerOpTypes } from '@vue/reactivity'
export type Slot = (
...args: IfAny
@@ -196,6 +198,7 @@ export const updateSlots = (
// Parent was HMR updated so slot content may have changed.
// force update slots and mark instance for hmr as well
extend(slots, children as Slots)
+ trigger(instance, TriggerOpTypes.SET, '$slots')
} else if (optimized && type === SlotFlags.STABLE) {
// compiled AND stable.
// no need to update, and skip stale slots removal.