-
-
Notifications
You must be signed in to change notification settings - Fork 8.4k
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): proxyRefs
method and ShallowUnwrapRefs
type
#1682
Conversation
BREAKING CHANGE: template auto ref unwrapping are now applied shallowly, i.e. only at the root level. This change aims to ensure that non-ref values referenced in templates retain the same identity with the value declared inside `setup()` regardless of whether it's wrapped with `reactive` or `readonly`. The breaking case is that given the following: ```js setup() { return { one: ref(1), two: { three: ref(3) }, four: 4 } } ``` After this change, `{{ one }}` in the template will still render `1`, but `{{ two.three }}` will no longer be auto unwrapped as `3`. A common case where this could happen is returning an object of refs from a composition function. The recommendation is to either expose the refs directly, or call the newly exposed `proxyRefs` on the object so that the object's refs are unwrapped like root level refs. In fact, `proxyRefs` is also the method used on the `setup()` return object. This also makes non-ref properties in the returned objects non-reactive, so mutating `four` from the template will no longer trigger updates.
@@ -561,7 +561,7 @@ export function handleSetupResult( | |||
} | |||
// setup returned bindings. | |||
// assuming a render function compiled from template is present. | |||
instance.setupState = reactive(setupResult) | |||
instance.setupState = proxyRefs(setupResult) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yyx990803 is it on purpose that proxyRefs
doesn't check if its argument is reactive and always wraps it with a new proxy?
I very often return state that is reactive({})
and it seems wasteful to always go 2 layers deep in proxies to access state, when just one would be perfectly fine.
I'd rather have proxyRefs
be a default fallback when I pass a plain object.
Or going full circle to one of my very first suggestions: not wrapping at all and leaving the responsibility of picking a wrapper to setup
. (it seems reasonable that <script setup>
always wraps the exports in a proxyRefs
).
Yup this was discussed at great lengths during the alpha, so I'm a bit surprised by the late change! I think we established that with proxies there will always be unfortunate edge cases but there were 2 "safer" patterns:
This change feels a bit like a middle ground to me, which is arguably the worst option because you get all the drawbacks of every other option.
I won't be one to complain about this change as it rather goes into the direction I preferred, but it doesn't feel totally "right". This change supports patterns that deviate from the previous recommendation, which makes me wonder: what is the new recommended pattern that is generally safe? |
Auto un-ref is mostly an ergonomic need at the root level, because it is only at the root level you are likely to get a mix of plain objects, reactive objects and refs.
No. This change really only affects a single case: plain objects at the root level - which most of the time is intentional, and only this case leads to the confusing behavior discussed in the rationale. This change fixes exactly those cases.
As long as you expose That said, the motivation is to fix the confusing cases mentioned above, which are much more confusing than "ref unwrapping only works at root level" - the latter is a rather straightforward rule to remember. |
Wow! This looks great, but it may be possible to improve it even more: I made an implementation of something similar as an experiment, and the nested auto unref also worked there. It was done by also unreffing in the component proxy (RobbinBaauw@d4653a9#diff-cc6f02998e28ee54ec621c0be5c7b5caL230). This means that the last level of refs is unwrapped, irrespective of the depth. This however has the downside that if you make the nested object a setup() {
return {
rootRef: ref(1),
object: {
nestedRef: ref(3)
}
}
} will work, whereas setup() {
return {
rootRef: ref(1),
object: {
nestedObjectRef: ref({
nestedRef: ref(3)
})
}
}
} will not, as the nested object is not unreffed. So on the one side this is an improvement, but on the other side it creates new edge cases that would have to be explained (even though an object ref will probably not be common) |
@RobbinBaauw I may be mistaken but your approach doesn't seem to do deep unwrap (i.e. |
@yyx990803 I believe that with the I don't have access to a pc currently though, so I'm not 100% sure, however iirc something similar to your example worked on that branch (once I get home I'll verify this). Even though I think this covers many cases, the "only the first layer is unreffed" may be more friendly to end users as there is a clear line as to what is automatically unreffed and what is not. |
Why is this a serious problem that must be resolved? We have an application in which we use a datamodel (classes with properties and methods) and tend to provide these in the setup context, which can then be used. But when the There are two possible solutions from a user perspective:
To be honest, we mix both workarounds right now. But solution 1 is really easy to forget. You may experience strange errors as described by @RobbinBaauw that take you a couple of days to understand.. But there is a far worse implication: doing it wrong only once destroys all JS engine optimizations without noticing it (see example). Solution 2 can be quite tedious. It leads to far more verbose code than just adding a
To me it feels quite the opposite. It is the best of both worlds:
So I am really happy with this PR. But it could be even better. Just drop the component proxy and always reference the setup raw object as |
export type ShallowUnwrapRef<T> = { | ||
[K in keyof T]: T[K] extends Ref<infer V> ? V : T[K] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this is better ?
export type ShallowUnwrapRef<T> = T extends ReturnType<typeof reactive>
? T
: { [K in keyof T]: T[K] extends Ref<infer V> ? V : T[K] }
BREAKING CHANGE: template auto ref unwrapping are now applied shallowly, i.e. only at the root level. See vuejs/core#1682 for more details.
* feat: `proxyRefs` method and `ShallowUnwrapRefs` type BREAKING CHANGE: template auto ref unwrapping are now applied shallowly, i.e. only at the root level. See vuejs/core#1682 for more details.
After this or some later changes I'm seeing weird behavior with my refs being unwrapped in watchers. For example: <template>
<div>Check the console</div>
</template>
<script>
import { reactive, watchEffect, ref } from "@vue/composition-api";
export default {
setup() {
const temp = ref("xxx");
const state = {
value: ref(0)
};
watchEffect(() => {
console.log("state.value:", state.value);
temp.value = "";
});
return {
state: reactive(state)
};
}
};
</script> When you open the console you'll see that the first time the It only happens when returning "reactive" Here is live demo: https://codesandbox.io/s/lucid-hooks-y48qt?file=/src/App.vue:0-408 |
@rchl this is v3 repo and your issue is with the composition api plugin. Vue2 reactivity system mutates the original object, so when you do reactive will change 6our state object and unwrap it, this as nothing to do with the template. |
My bad, will report there. |
* feat: `proxyRefs` method and `ShallowUnwrapRefs` type BREAKING CHANGE: template auto ref unwrapping are now applied shallowly, i.e. only at the root level. See vuejs/core#1682 for more details.
BREAKING CHANGE
Template auto ref unwrapping for
setup()
return object is now applied only to the root level refs.Rationale
This change aims to ensure that non-ref values referenced in templates retain the same identity with the value declared inside
setup()
regardless of whether it's wrapped withreactive
orreadonly
.Currently, the object returned from
setup()
is exposed to the template render context as areactive
proxy. This means all properties accessed from the template, even nested ones, will be reactive proxies as well. This has led to a edge cases that can be confusing:live demo
Before this change, the render result will be
false false
because theitem
accessed in the template are proxies and not strictly equal to the original items declared insetup()
.Another case involves class instances exposed to templates:
live demo
Here,
this
would point to the proxy infoo.getValue
because thefoo
in the template is a proxied version of the class instance, and this results inthis.count
being an already unwrapped number, andthis.count.value
will beundefined
(hence the render reuslt will be blank). In other words, the behavior of the class becomes inconsistent based on whether it's accessed through the template or not.We have went back-and-forth on this behavior during the alpha/beta phases. Previously, we felt we could encourage users to always explicitly wrap/mark returned objects with
markRaw
,reactive
orreadonly
to avoid such problems, but it always feel like a potential footgun. Although we indicated that we are trying to avoid breaking changes during the RC phase, we feel this it is important to have this discussed before the final release.Breakage Details
The breaking case is that given the following:
After this change,
{{ rootRef }}
in the template will still render1
, but{{ object.nestedRef }}
will no longer be auto unwrapped as3
.Composition function returning plain object with refs
A common case where this could happen is returning an object of refs from a composition function:
The fix is rather straightforward: wrap it with
reactive
to unwrap the refs:Another aspect of this is that users probably do this to avoid the trouble of destructuring and then returning the setup object, which can become tedious if there are many properties in the returned object:
which can be simplified in
<script setup>
as:Setup returned properties are no longer reactive
Another minor breakage is that this also makes non-ref properties in the returned objects non-reactive:
Mutating
count
from the template, e.g. with<button @click="count++">
will no longer trigger updates./cc @jods4 @RobbinBaauw @basvanmeurs