-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(reactivity): improve support of getter usage in reactivity APIs #7997
Conversation
/ecosystem-ci run |
📝 Ran ecosystem CI: Open
|
I like this! I guess we will be able to write: export function useQuery (variables) {
watch(toRef(variables), (value) => {})
}
useQuery(() => props.foo) ? |
Yes - although I'd write: export function useQuery (variables) {
watch(() => toValue(variables), (value) => {})
} which would be more efficient as it doesn't create an intermediate ref. |
@yyx990803 why introduce yet another way to transport a single value when we already have one? I think 2 cases is enough, what would be wrong with an API that creates another ref-like: const bar = toRef(() => props.foo.bar);
// Basically:
// bar = { get value() { return props.foo.bar } }
// We can use the `bar` value, just like any other ref today:
const value = unref(bar);
// or inside templates and it's automatically unwrapped. |
I'm a big fan that The improved naming and enhanced DX to the pair seem like a solid step forward for the ecosystem. Great work on this! 🙌 |
@skirtles-code great review, fixed! @jods4 getters only happen at the boundaries between component setup code and composables. There is no need to unwrap them in templates. Always forcing refs creates inefficiency by allocating extra one-off refs and syntax fluff. Passing getters and normalizing into values at the effect call-site is more efficient, and composable authors can handle these cases by simply using |
Sorry, I may not understand correctly, but it could have worked like this? export function useQuery (variables) {
watch(variables, (value) => {})
} |
@yyx990803 if it's already widely use then I guess that's reason enough... I read the whole proposal again and I see in which contexts you mean to use it, I guess it makes sense. Reading your comment about how typical Looking forward to 3.3, between props deconstruction, this and generic components it's gonna be a great release! |
Currently, My code uses it to interface with a Pinia store. Example: const size = toRef(store, "fontSize");
const increase = () => {
size.value = clamp(size.value + change, min, max);
};
const decrease = () => {
size.value = clamp(size.value - change, min, max);
}; |
TL;DR
toValue()
)toRef()
)MaybeRef<T>
andMaybeRefOrGetter<T>
typesContext
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:
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:The above code has two problems:
props.foo
may not exist whentoRef
is calledprops.foo
is swapped to a different object.To workaround this, we can use
computed
: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):
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:
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:
To also support getters, the accepted type will be:
We currently provide
unref
that normalizesMaybeRef<T>
toT
. However, we cannot makeunref
also unwrap getters, because it would be a breaking change. It's possible to callunref
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()
:This is equivalent to VueUse's resolveUnref(). The
toValue()
name here is chosen since it is the opposite oftoRef()
: the two stands for two different directions of normalization: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: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":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 onvue-demi
.If it is backported to 2.7, VueUse can also replace
resolveRef
andresolveUnref
withtoRef
andtoValue
.