Skip to content

Commit 8560005

Browse files
committed
fix(runtime-core): ensure setupContext.attrs reactivity when used in child slots
fix #4161
1 parent ff0c810 commit 8560005

File tree

2 files changed

+72
-19
lines changed

2 files changed

+72
-19
lines changed

packages/runtime-core/__tests__/apiSetupContext.spec.ts

+38
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,44 @@ describe('api: setup context', () => {
135135
expect(serializeInner(root)).toMatch(`<div class="baz"></div>`)
136136
})
137137

138+
// #4161
139+
it('context.attrs in child component slots', async () => {
140+
const toggle = ref(true)
141+
142+
const Parent = {
143+
render: () => h(Child, toggle.value ? { id: 'foo' } : { class: 'baz' })
144+
}
145+
146+
const Wrapper = {
147+
render(this: any) {
148+
return this.$slots.default()
149+
}
150+
}
151+
152+
const Child = {
153+
inheritAttrs: false,
154+
setup(_: any, { attrs }: any) {
155+
return () => {
156+
const vnode = h(Wrapper, null, {
157+
default: () => [h('div', attrs)],
158+
_: 1 // mark stable slots
159+
})
160+
vnode.dynamicChildren = [] // force optimized mode
161+
return vnode
162+
}
163+
}
164+
}
165+
166+
const root = nodeOps.createElement('div')
167+
render(h(Parent), root)
168+
expect(serializeInner(root)).toMatch(`<div id="foo"></div>`)
169+
170+
// should update even though it's not reactive
171+
toggle.value = false
172+
await nextTick()
173+
expect(serializeInner(root)).toMatch(`<div class="baz"></div>`)
174+
})
175+
138176
it('context.slots', async () => {
139177
const id = ref('foo')
140178

packages/runtime-core/src/component.ts

+34-19
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
shallowReadonly,
66
proxyRefs,
77
EffectScope,
8-
markRaw
8+
markRaw,
9+
track,
10+
TrackOpTypes
911
} from '@vue/reactivity'
1012
import {
1113
ComponentPublicInstance,
@@ -834,19 +836,32 @@ export function finishComponentSetup(
834836
}
835837
}
836838

837-
const attrDevProxyHandlers: ProxyHandler<Data> = {
838-
get: (target, key: string) => {
839-
markAttrsAccessed()
840-
return target[key]
841-
},
842-
set: () => {
843-
warn(`setupContext.attrs is readonly.`)
844-
return false
845-
},
846-
deleteProperty: () => {
847-
warn(`setupContext.attrs is readonly.`)
848-
return false
849-
}
839+
function createAttrsProxy(instance: ComponentInternalInstance): Data {
840+
return new Proxy(
841+
instance.attrs,
842+
__DEV__
843+
? {
844+
get(target, key: string) {
845+
markAttrsAccessed()
846+
track(instance, TrackOpTypes.GET, '$attrs')
847+
return target[key]
848+
},
849+
set() {
850+
warn(`setupContext.attrs is readonly.`)
851+
return false
852+
},
853+
deleteProperty() {
854+
warn(`setupContext.attrs is readonly.`)
855+
return false
856+
}
857+
}
858+
: {
859+
get(target, key: string) {
860+
track(instance, TrackOpTypes.GET, '$attrs')
861+
return target[key]
862+
}
863+
}
864+
)
850865
}
851866

852867
export function createSetupContext(
@@ -859,15 +874,13 @@ export function createSetupContext(
859874
instance.exposed = exposed || {}
860875
}
861876

877+
let attrs: Data
862878
if (__DEV__) {
863-
let attrs: Data
864879
// We use getters in dev in case libs like test-utils overwrite instance
865880
// properties (overwrites should not be done in prod)
866881
return Object.freeze({
867882
get attrs() {
868-
return (
869-
attrs || (attrs = new Proxy(instance.attrs, attrDevProxyHandlers))
870-
)
883+
return attrs || (attrs = createAttrsProxy(instance))
871884
},
872885
get slots() {
873886
return shallowReadonly(instance.slots)
@@ -879,7 +892,9 @@ export function createSetupContext(
879892
})
880893
} else {
881894
return {
882-
attrs: instance.attrs,
895+
get attrs() {
896+
return attrs || (attrs = createAttrsProxy(instance))
897+
},
883898
slots: instance.slots,
884899
emit: instance.emit,
885900
expose

0 commit comments

Comments
 (0)