feat(reactivity): improve support of getter usage in reactivity APIs by yyx990803 · Pull Request #7997 · vuejs/core (original) (raw)
TL;DR
- One API for normalizing difference sources (value / ref / getter) to values (by introducing
toValue()
) - One API for normalizing different sources (value / ref / getter) to refs (by enhancing
toRef()
) - Introduce
MaybeRef<T>
andMaybeRefOrGetter<T>
types
Context
Why Getters
It is common that we need to pass state into the composable and retain reactivity. In most cases, this means converting a reactive source into a ref:
import { toRef } from 'vue'
const props = defineProps(/* ... */)
useFeature(toRef(props, 'foo'))
Currently, toRef
is only used to "pluck" a single property from an object. It is also a bit inflexible - for example, if we want to convert a nested property to a ref:
useFeature(toRef(props.foo, 'bar'))
The above code has two problems:
props.foo
may not exist whentoRef
is called- This cannot handle the case if
props.foo
is swapped to a different object.
To workaround this, we can use computed
:
useFeature(computed(() => props.foo?.bar))
However, using computed
is sub-optimal here. Internally, computed
creates a separate effect to cache the computation. This actually is largely overhead when the getter is simply accessing properties and not performing any expensive computations.
The least expensive way to pass non-ref reactive state into a composable is by wrapping it with a getter (or "thunking" - i.e. delaying the access of the actual value until the getter is called):
useFeature(() => props.foo?.bar)
VueUse already supports this pattern extensively. This is also a bit similar to function-style signals as seen in Solid.
In addition, this pattern will be quite commonly used when using reactive props destructure:
const { foo } = defineProps<{ foo: string }>()
useFeature(() => foo)
Introducing toValue()
On the comspoable side, it is already common for composables to accept arguments that could either be a value or a ref. This can be represented by:
type MaybeRef = T | Ref
To also support getters, the accepted type will be:
type MaybeRefOrGetter = MaybeRef | (() => T)
We currently provide unref
that normalizes MaybeRef<T>
to T
. However, we cannot make unref
also unwrap getters, because it would be a breaking change. It's possible to call unref
on a function value and expect to get the function back. This is relatively rare, but it is still a possible case that we cannot break.
So, we introduce a new method, toValue()
:
export function toValue(source: MaybeRefOrGetter): T { return isFunction(source) ? source() : unref(source) }
This is equivalent to VueUse's resolveUnref(). The toValue()
name here is chosen since it is the opposite of toRef()
: the two stands for two different directions of normalization:
ref <- toRef() - ref/value/getter - toValue() -> value
Enhancing toRef()
There maybe cases where a ref is required - you just can't pass a getter. We can still use computed
for this case, but as mentioned, computed
is an overkill for simple getters that just access properties.
We can add new overloads to toRef()
so that it can now take getters:
const ref = toRef(() => 123) ref.value // 123
The ref created this way is readonly and just invokes the getter on every .value
access.
We also mentioned that toRef()
should now be considered the API for "normalizing value / ref / getter to refs":
// value -> Ref toRef(1) // Ref
// Ref -> Ref toRef(ref(1)) // Ref
// getter -> Ref toRef(() => 1) // Ref
This is equivalent to VueUse's resolveRef().
The old toRef(object, 'key')
usage is still supported, but the more flexible getter syntax should be preferred:
Adoption Concerns
Backport to 2.7?
These additions create discrepancy between 3.3 and 2.7 - although in theory we are no longer adding new features to 2.7, these are probably worth backporting to ensure behavior consistency of vue-demi
, and VueUse which relies on vue-demi
.
If it is backported to 2.7, VueUse can also replace resolveRef
and resolveUnref
with toRef
and toValue
.