Skip to content

Commit 59e8284

Browse files
authored
feat(reactivity): improve support of getter usage in reactivity APIs (#7997)
1 parent dfb21a5 commit 59e8284

File tree

8 files changed

+237
-35
lines changed

8 files changed

+237
-35
lines changed

packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts

+31-3
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ describe('sfc props transform', () => {
294294
).toThrow(`Cannot assign to destructured props`)
295295
})
296296

297-
test('should error when watching destructured prop', () => {
297+
test('should error when passing destructured prop into certain methods', () => {
298298
expect(() =>
299299
compile(
300300
`<script setup>
@@ -303,7 +303,9 @@ describe('sfc props transform', () => {
303303
watch(foo, () => {})
304304
</script>`
305305
)
306-
).toThrow(`"foo" is a destructured prop and cannot be directly watched.`)
306+
).toThrow(
307+
`"foo" is a destructured prop and should not be passed directly to watch().`
308+
)
307309

308310
expect(() =>
309311
compile(
@@ -313,7 +315,33 @@ describe('sfc props transform', () => {
313315
w(foo, () => {})
314316
</script>`
315317
)
316-
).toThrow(`"foo" is a destructured prop and cannot be directly watched.`)
318+
).toThrow(
319+
`"foo" is a destructured prop and should not be passed directly to watch().`
320+
)
321+
322+
expect(() =>
323+
compile(
324+
`<script setup>
325+
import { toRef } from 'vue'
326+
const { foo } = defineProps(['foo'])
327+
toRef(foo)
328+
</script>`
329+
)
330+
).toThrow(
331+
`"foo" is a destructured prop and should not be passed directly to toRef().`
332+
)
333+
334+
expect(() =>
335+
compile(
336+
`<script setup>
337+
import { toRef as r } from 'vue'
338+
const { foo } = defineProps(['foo'])
339+
r(foo)
340+
</script>`
341+
)
342+
).toThrow(
343+
`"foo" is a destructured prop and should not be passed directly to toRef().`
344+
)
317345
})
318346

319347
// not comprehensive, but should help for most common cases

packages/compiler-sfc/src/compileScript.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1442,7 +1442,7 @@ export function compileScript(
14421442
startOffset,
14431443
propsDestructuredBindings,
14441444
error,
1445-
vueImportAliases.watch
1445+
vueImportAliases
14461446
)
14471447
}
14481448

packages/compiler-sfc/src/compileScriptPropsDestructure.ts

+16-11
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function transformDestructuredProps(
3232
offset = 0,
3333
knownProps: PropsDestructureBindings,
3434
error: (msg: string, node: Node, end?: number) => never,
35-
watchMethodName = 'watch'
35+
vueImportAliases: Record<string, string>
3636
) {
3737
const rootScope: Scope = {}
3838
const scopeStack: Scope[] = [rootScope]
@@ -152,6 +152,19 @@ export function transformDestructuredProps(
152152
return false
153153
}
154154

155+
function checkUsage(node: Node, method: string, alias = method) {
156+
if (isCallOf(node, alias)) {
157+
const arg = unwrapTSNode(node.arguments[0])
158+
if (arg.type === 'Identifier') {
159+
error(
160+
`"${arg.name}" is a destructured prop and should not be passed directly to ${method}(). ` +
161+
`Pass a getter () => ${arg.name} instead.`,
162+
arg
163+
)
164+
}
165+
}
166+
}
167+
155168
// check root scope first
156169
walkScope(ast, true)
157170
;(walk as any)(ast, {
@@ -169,16 +182,8 @@ export function transformDestructuredProps(
169182
return this.skip()
170183
}
171184

172-
if (isCallOf(node, watchMethodName)) {
173-
const arg = unwrapTSNode(node.arguments[0])
174-
if (arg.type === 'Identifier') {
175-
error(
176-
`"${arg.name}" is a destructured prop and cannot be directly watched. ` +
177-
`Use a getter () => ${arg.name} instead.`,
178-
arg
179-
)
180-
}
181-
}
185+
checkUsage(node, 'watch', vueImportAliases.watch)
186+
checkUsage(node, 'toRef', vueImportAliases.toRef)
182187

183188
// function scopes
184189
if (isFunctionType(node)) {

packages/dts-test/ref.test-d.ts

+70-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ import {
77
reactive,
88
proxyRefs,
99
toRef,
10+
toValue,
1011
toRefs,
1112
ToRefs,
1213
shallowReactive,
13-
readonly
14+
readonly,
15+
MaybeRef,
16+
MaybeRefOrGetter,
17+
ComputedRef,
18+
computed
1419
} from 'vue'
1520
import { expectType, describe } from './utils'
1621

@@ -26,6 +31,8 @@ function plainType(arg: number | Ref<number>) {
2631

2732
// ref unwrapping
2833
expectType<number>(unref(arg))
34+
expectType<number>(toValue(arg))
35+
expectType<number>(toValue(() => 123))
2936

3037
// ref inner type should be unwrapped
3138
const nestedRef = ref({
@@ -203,6 +210,13 @@ expectType<Ref<string>>(p2.obj.k)
203210
// Should not distribute Refs over union
204211
expectType<Ref<number | string>>(toRef(obj, 'c'))
205212

213+
expectType<Ref<number>>(toRef(() => 123))
214+
expectType<Ref<number | string>>(toRef(() => obj.c))
215+
216+
const r = toRef(() => 123)
217+
// @ts-expect-error
218+
r.value = 234
219+
206220
// toRefs
207221
expectType<{
208222
a: Ref<number>
@@ -319,3 +333,58 @@ describe('reactive in shallow ref', () => {
319333

320334
expectType<number>(x.value.a.b)
321335
})
336+
337+
describe('toRef <-> toValue', () => {
338+
function foo(
339+
a: MaybeRef<string>,
340+
b: () => string,
341+
c: MaybeRefOrGetter<string>,
342+
d: ComputedRef<string>
343+
) {
344+
const r = toRef(a)
345+
expectType<Ref<string>>(r)
346+
// writable
347+
r.value = 'foo'
348+
349+
const rb = toRef(b)
350+
expectType<Readonly<Ref<string>>>(rb)
351+
// @ts-expect-error ref created from getter should be readonly
352+
rb.value = 'foo'
353+
354+
const rc = toRef(c)
355+
expectType<Readonly<Ref<string> | Ref<string>>>(rc)
356+
// @ts-expect-error ref created from MaybeReadonlyRef should be readonly
357+
rc.value = 'foo'
358+
359+
const rd = toRef(d)
360+
expectType<ComputedRef<string>>(rd)
361+
// @ts-expect-error ref created from computed ref should be readonly
362+
rd.value = 'foo'
363+
364+
expectType<string>(toValue(a))
365+
expectType<string>(toValue(b))
366+
expectType<string>(toValue(c))
367+
expectType<string>(toValue(d))
368+
369+
return {
370+
r: toValue(r),
371+
rb: toValue(rb),
372+
rc: toValue(rc),
373+
rd: toValue(rd)
374+
}
375+
}
376+
377+
expectType<{
378+
r: string
379+
rb: string
380+
rc: string
381+
rd: string
382+
}>(
383+
foo(
384+
'foo',
385+
() => 'bar',
386+
ref('baz'),
387+
computed(() => 'hi')
388+
)
389+
)
390+
})

packages/reactivity/__tests__/ref.spec.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import {
1111
} from '../src/index'
1212
import { computed } from '@vue/runtime-dom'
1313
import { shallowRef, unref, customRef, triggerRef } from '../src/ref'
14-
import { isShallow, readonly, shallowReactive } from '../src/reactive'
14+
import {
15+
isReadonly,
16+
isShallow,
17+
readonly,
18+
shallowReactive
19+
} from '../src/reactive'
1520

1621
describe('reactivity/ref', () => {
1722
it('should hold a value', () => {
@@ -275,6 +280,15 @@ describe('reactivity/ref', () => {
275280
expect(toRef(r, 'x')).toBe(r.x)
276281
})
277282

283+
test('toRef on array', () => {
284+
const a = reactive(['a', 'b'])
285+
const r = toRef(a, 1)
286+
expect(r.value).toBe('b')
287+
r.value = 'c'
288+
expect(r.value).toBe('c')
289+
expect(a[1]).toBe('c')
290+
})
291+
278292
test('toRef default value', () => {
279293
const a: { x: number | undefined } = { x: undefined }
280294
const x = toRef(a, 'x', 1)
@@ -287,6 +301,17 @@ describe('reactivity/ref', () => {
287301
expect(x.value).toBe(1)
288302
})
289303

304+
test('toRef getter', () => {
305+
const x = toRef(() => 1)
306+
expect(x.value).toBe(1)
307+
expect(isRef(x)).toBe(true)
308+
expect(unref(x)).toBe(1)
309+
//@ts-expect-error
310+
expect(() => (x.value = 123)).toThrow()
311+
312+
expect(isReadonly(x)).toBe(true)
313+
})
314+
290315
test('toRefs', () => {
291316
const a = reactive({
292317
x: 1,

packages/reactivity/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ export {
33
shallowRef,
44
isRef,
55
toRef,
6+
toValue,
67
toRefs,
78
unref,
89
proxyRefs,
910
customRef,
1011
triggerRef,
1112
type Ref,
13+
type MaybeRef,
14+
type MaybeRefOrGetter,
1215
type ToRef,
1316
type ToRefs,
1417
type UnwrapRef,

0 commit comments

Comments
 (0)