diff --git a/packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts b/packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts
index 346f95a5c7a..8487270c265 100644
--- a/packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts
+++ b/packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts
@@ -294,7 +294,7 @@ describe('sfc props transform', () => {
).toThrow(`Cannot assign to destructured props`)
})
- test('should error when watching destructured prop', () => {
+ test('should error when passing destructured prop into certain methods', () => {
expect(() =>
compile(
``
)
- ).toThrow(`"foo" is a destructured prop and cannot be directly watched.`)
+ ).toThrow(
+ `"foo" is a destructured prop and should not be passed directly to watch().`
+ )
expect(() =>
compile(
@@ -313,7 +315,33 @@ describe('sfc props transform', () => {
w(foo, () => {})
`
)
- ).toThrow(`"foo" is a destructured prop and cannot be directly watched.`)
+ ).toThrow(
+ `"foo" is a destructured prop and should not be passed directly to watch().`
+ )
+
+ expect(() =>
+ compile(
+ ``
+ )
+ ).toThrow(
+ `"foo" is a destructured prop and should not be passed directly to toRef().`
+ )
+
+ expect(() =>
+ compile(
+ ``
+ )
+ ).toThrow(
+ `"foo" is a destructured prop and should not be passed directly to toRef().`
+ )
})
// not comprehensive, but should help for most common cases
diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
index de9e11d071f..ec476c4ad16 100644
--- a/packages/compiler-sfc/src/compileScript.ts
+++ b/packages/compiler-sfc/src/compileScript.ts
@@ -1442,7 +1442,7 @@ export function compileScript(
startOffset,
propsDestructuredBindings,
error,
- vueImportAliases.watch
+ vueImportAliases
)
}
diff --git a/packages/compiler-sfc/src/compileScriptPropsDestructure.ts b/packages/compiler-sfc/src/compileScriptPropsDestructure.ts
index bc38912653e..4ee09070d76 100644
--- a/packages/compiler-sfc/src/compileScriptPropsDestructure.ts
+++ b/packages/compiler-sfc/src/compileScriptPropsDestructure.ts
@@ -32,7 +32,7 @@ export function transformDestructuredProps(
offset = 0,
knownProps: PropsDestructureBindings,
error: (msg: string, node: Node, end?: number) => never,
- watchMethodName = 'watch'
+ vueImportAliases: Record
) {
const rootScope: Scope = {}
const scopeStack: Scope[] = [rootScope]
@@ -152,6 +152,19 @@ export function transformDestructuredProps(
return false
}
+ function checkUsage(node: Node, method: string, alias = method) {
+ if (isCallOf(node, alias)) {
+ const arg = unwrapTSNode(node.arguments[0])
+ if (arg.type === 'Identifier') {
+ error(
+ `"${arg.name}" is a destructured prop and should not be passed directly to ${method}(). ` +
+ `Pass a getter () => ${arg.name} instead.`,
+ arg
+ )
+ }
+ }
+ }
+
// check root scope first
walkScope(ast, true)
;(walk as any)(ast, {
@@ -169,16 +182,8 @@ export function transformDestructuredProps(
return this.skip()
}
- if (isCallOf(node, watchMethodName)) {
- const arg = unwrapTSNode(node.arguments[0])
- if (arg.type === 'Identifier') {
- error(
- `"${arg.name}" is a destructured prop and cannot be directly watched. ` +
- `Use a getter () => ${arg.name} instead.`,
- arg
- )
- }
- }
+ checkUsage(node, 'watch', vueImportAliases.watch)
+ checkUsage(node, 'toRef', vueImportAliases.toRef)
// function scopes
if (isFunctionType(node)) {
diff --git a/packages/dts-test/ref.test-d.ts b/packages/dts-test/ref.test-d.ts
index dbf54de09c8..bbcde45ddda 100644
--- a/packages/dts-test/ref.test-d.ts
+++ b/packages/dts-test/ref.test-d.ts
@@ -7,10 +7,15 @@ import {
reactive,
proxyRefs,
toRef,
+ toValue,
toRefs,
ToRefs,
shallowReactive,
- readonly
+ readonly,
+ MaybeRef,
+ MaybeRefOrGetter,
+ ComputedRef,
+ computed
} from 'vue'
import { expectType, describe } from './utils'
@@ -26,6 +31,8 @@ function plainType(arg: number | Ref) {
// ref unwrapping
expectType(unref(arg))
+ expectType(toValue(arg))
+ expectType(toValue(() => 123))
// ref inner type should be unwrapped
const nestedRef = ref({
@@ -203,6 +210,13 @@ expectType[>(p2.obj.k)
// Should not distribute Refs over union
expectType][>(toRef(obj, 'c'))
+ expectType][>(toRef(() => 123))
+ expectType][>(toRef(() => obj.c))
+
+ const r = toRef(() => 123)
+ // @ts-expect-error
+ r.value = 234
+
// toRefs
expectType<{
a: Ref
@@ -319,3 +333,58 @@ describe('reactive in shallow ref', () => {
expectType(x.value.a.b)
})
+
+describe('toRef <-> toValue', () => {
+ function foo(
+ a: MaybeRef,
+ b: () => string,
+ c: MaybeRefOrGetter,
+ d: ComputedRef
+ ) {
+ const r = toRef(a)
+ expectType][>(r)
+ // writable
+ r.value = 'foo'
+
+ const rb = toRef(b)
+ expectType>>(rb)
+ // @ts-expect-error ref created from getter should be readonly
+ rb.value = 'foo'
+
+ const rc = toRef(c)
+ expectType | Ref>>(rc)
+ // @ts-expect-error ref created from MaybeReadonlyRef should be readonly
+ rc.value = 'foo'
+
+ const rd = toRef(d)
+ expectType>(rd)
+ // @ts-expect-error ref created from computed ref should be readonly
+ rd.value = 'foo'
+
+ expectType(toValue(a))
+ expectType(toValue(b))
+ expectType(toValue(c))
+ expectType(toValue(d))
+
+ return {
+ r: toValue(r),
+ rb: toValue(rb),
+ rc: toValue(rc),
+ rd: toValue(rd)
+ }
+ }
+
+ expectType<{
+ r: string
+ rb: string
+ rc: string
+ rd: string
+ }>(
+ foo(
+ 'foo',
+ () => 'bar',
+ ref('baz'),
+ computed(() => 'hi')
+ )
+ )
+})
diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts
index 646cc6e6791..718b2bc61b8 100644
--- a/packages/reactivity/__tests__/ref.spec.ts
+++ b/packages/reactivity/__tests__/ref.spec.ts
@@ -11,7 +11,12 @@ import {
} from '../src/index'
import { computed } from '@vue/runtime-dom'
import { shallowRef, unref, customRef, triggerRef } from '../src/ref'
-import { isShallow, readonly, shallowReactive } from '../src/reactive'
+import {
+ isReadonly,
+ isShallow,
+ readonly,
+ shallowReactive
+} from '../src/reactive'
describe('reactivity/ref', () => {
it('should hold a value', () => {
@@ -275,6 +280,15 @@ describe('reactivity/ref', () => {
expect(toRef(r, 'x')).toBe(r.x)
})
+ test('toRef on array', () => {
+ const a = reactive(['a', 'b'])
+ const r = toRef(a, 1)
+ expect(r.value).toBe('b')
+ r.value = 'c'
+ expect(r.value).toBe('c')
+ expect(a[1]).toBe('c')
+ })
+
test('toRef default value', () => {
const a: { x: number | undefined } = { x: undefined }
const x = toRef(a, 'x', 1)
@@ -287,6 +301,17 @@ describe('reactivity/ref', () => {
expect(x.value).toBe(1)
})
+ test('toRef getter', () => {
+ const x = toRef(() => 1)
+ expect(x.value).toBe(1)
+ expect(isRef(x)).toBe(true)
+ expect(unref(x)).toBe(1)
+ //@ts-expect-error
+ expect(() => (x.value = 123)).toThrow()
+
+ expect(isReadonly(x)).toBe(true)
+ })
+
test('toRefs', () => {
const a = reactive({
x: 1,
diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts
index 60707febef4..ee4da5b1935 100644
--- a/packages/reactivity/src/index.ts
+++ b/packages/reactivity/src/index.ts
@@ -3,12 +3,15 @@ export {
shallowRef,
isRef,
toRef,
+ toValue,
toRefs,
unref,
proxyRefs,
customRef,
triggerRef,
type Ref,
+ type MaybeRef,
+ type MaybeRefOrGetter,
type ToRef,
type ToRefs,
type UnwrapRef,
diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts
index 85a19802d4f..5dd31a9f8ca 100644
--- a/packages/reactivity/src/ref.ts
+++ b/packages/reactivity/src/ref.ts
@@ -6,7 +6,7 @@ import {
triggerEffects
} from './effect'
import { TrackOpTypes, TriggerOpTypes } from './operations'
-import { isArray, hasChanged, IfAny } from '@vue/shared'
+import { isArray, hasChanged, IfAny, isFunction, isObject } from '@vue/shared'
import {
isProxy,
toRaw,
@@ -87,9 +87,7 @@ export function isRef(r: any): r is Ref {
* @param value - The object to wrap in the ref.
* @see {@link https://vuejs.org/api/reactivity-core.html#ref}
*/
-export function ref(
- value: T
-): [T] extends [Ref] ? T : Ref>
+export function ref(value: T): T
export function ref(value: T): Ref>
export function ref(): Ref
export function ref(value?: unknown) {
@@ -191,6 +189,9 @@ export function triggerRef(ref: Ref) {
triggerRefValue(ref, __DEV__ ? ref.value : void 0)
}
+export type MaybeRef = T | Ref
+export type MaybeRefOrGetter = MaybeRef | (() => T)
+
/**
* Returns the inner value if the argument is a ref, otherwise return the
* argument itself. This is a sugar function for
@@ -207,10 +208,30 @@ export function triggerRef(ref: Ref) {
* @param ref - Ref or plain value to be converted into the plain value.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#unref}
*/
-export function unref(ref: T | Ref): T {
+export function unref(ref: MaybeRef): T {
return isRef(ref) ? (ref.value as any) : ref
}
+/**
+ * Normalizes values / refs / getters to values.
+ * This is similar to {@link unref()}, except that it also normalizes getters.
+ * If the argument is a getter, it will be invoked and its return value will
+ * be returned.
+ *
+ * @example
+ * ```js
+ * toValue(1) // 1
+ * toValue(ref(1)) // 1
+ * toValue(() => 1) // 1
+ * ```
+ *
+ * @param source - A getter, an existing ref, or a non-function value.
+ * @see {@link https://vuejs.org/api/reactivity-utilities.html#tovalue}
+ */
+export function toValue(source: MaybeRefOrGetter): T {
+ return isFunction(source) ? source() : unref(source)
+}
+
const shallowUnwrapHandlers: ProxyHandler = {
get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
set: (target, key, value, receiver) => {
@@ -305,7 +326,7 @@ export function toRefs(object: T): ToRefs {
}
const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
- ret[key] = toRef(object, key)
+ ret[key] = propertyToRef(object, key)
}
return ret
}
@@ -333,12 +354,36 @@ class ObjectRefImpl {
}
}
+class GetterRefImpl {
+ public readonly __v_isRef = true
+ public readonly __v_isReadonly = true
+ constructor(private readonly _getter: () => T) {}
+ get value() {
+ return this._getter()
+ }
+}
+
export type ToRef = IfAny, [T] extends [Ref] ? T : Ref>
/**
- * Can be used to create a ref for a property on a source reactive object. The
- * created ref is synced with its source property: mutating the source property
- * will update the ref, and vice-versa.
+ * Used to normalize values / refs / getters into refs.
+ *
+ * @example
+ * ```js
+ * // returns existing refs as-is
+ * toRef(existingRef)
+ *
+ * // creates a ref that calls the getter on .value access
+ * toRef(() => props.foo)
+ *
+ * // creates normal refs from non-function values
+ * // equivalent to ref(1)
+ * toRef(1)
+ * ```
+ *
+ * Can also be used to create a ref for a property on a source reactive object.
+ * The created ref is synced with its source property: mutating the source
+ * property will update the ref, and vice-versa.
*
* @example
* ```js
@@ -358,10 +403,18 @@ export type ToRef = IfAny, [T] extends [Ref] ? T : Ref>
* console.log(fooRef.value) // 3
* ```
*
- * @param object - The reactive object containing the desired property.
- * @param key - Name of the property in the reactive object.
+ * @param source - A getter, an existing ref, a non-function value, or a
+ * reactive object to create a property ref from.
+ * @param [key] - (optional) Name of the property in the reactive object.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#toref}
*/
+export function toRef(
+ value: T
+): T extends () => infer R
+ ? Readonly][>
+ : T extends Ref
+ ? T
+ : Ref>
export function toRef(
object: T,
key: K
@@ -371,15 +424,31 @@ export function toRef(
key: K,
defaultValue: T[K]
): ToRef>
-export function toRef(
- object: T,
- key: K,
- defaultValue?: T[K]
-): ToRef {
- const val = object[key]
+export function toRef(
+ source: Record | MaybeRef,
+ key?: string,
+ defaultValue?: unknown
+): Ref {
+ if (isRef(source)) {
+ return source
+ } else if (isFunction(source)) {
+ return new GetterRefImpl(source as () => unknown) as any
+ } else if (isObject(source) && arguments.length > 1) {
+ return propertyToRef(source, key!, defaultValue)
+ } else {
+ return ref(source)
+ }
+}
+
+function propertyToRef(source: object, key: string, defaultValue?: unknown) {
+ const val = (source as any)[key]
return isRef(val)
? val
- : (new ObjectRefImpl(object, key, defaultValue) as any)
+ : (new ObjectRefImpl(
+ source as Record,
+ key,
+ defaultValue
+ ) as any)
}
// corner case when use narrows type
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index 06f9a2affd4..936d6ca3565 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -11,6 +11,7 @@ export {
proxyRefs,
isRef,
toRef,
+ toValue,
toRefs,
isProxy,
isReactive,
@@ -152,6 +153,8 @@ declare module '@vue/reactivity' {
export { TrackOpTypes, TriggerOpTypes } from '@vue/reactivity'
export type {
Ref,
+ MaybeRef,
+ MaybeRefOrGetter,
ToRef,
ToRefs,
UnwrapRef,
]