-
Notifications
You must be signed in to change notification settings - Fork 546
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
When it's really needed to use toRefs
in order to retain reactivity of reactive
value
#145
Comments
toRefs
in order to retain reactivity of reactive
valuetoRefs
in order to retain reactivity of reactive
value
Consider an object that tracks the mouse position: f(mouse) {
let x = mouse.x;
// do stuff with x
} You don't expect the Also note that destructuring is not magical, it's syntactic sugar for assigning variables. It's a good analogy for reactivity. Vue 3 implements reactivity by wrapping proxies around your objects. Those proxies see reads and writes to their properties (which enables watching and triggering watches, resp.). So only going through the proxy is reactive, i.e. let x = mouse.x;
// This will not work:
watchEffect(() => console.log('The x mouse position is ', x)); And it is actually useful: let oldX = mouse.x;
// This works:
watchEffect(() => {
console.log(`The mouse moved ${mouse.x - oldX} pixels`);
oldX = mouse.x;
}); But sometimes you want to do what the first example did: extract one reactive value from a big reactive object. This is what Basically it does this transform: let mouse = reactive({ x: 0, y: 0 });
let mouseRefs = {
x: { get value() { return mouse.x }, set value(v) { mouse.x = v } }, // a ref that delegates to mouse
y: { /* same as x */ }
}
// now you can get a ref to mouse.x:
let x = mouseRefs.x; // this is a ref
x.value; // is actually watching mouse.x
x.value = 4; // is actually updating mouse.x in a reactive way. Why is this useful? Because sometimes you want to take apart an object and return or pass reactive values (i.e. refs) to functions. // This is a function that returns the computed length of a string (possibly ref)
// Let's say you got it from a library or shared code.
function len(s: string | Ref<string>) {
return computed(() => unref(s).length);
}
// This is a component of yours, that displays the length of its `name` props
const Component = {
setup(props) {
// name is a ref
const { name } = toRefs(props); // also toRefs(props).name works the same
// length is a computed
const length = len(name);
// can be bound in view
return { length };
}
}; Notice how doing
Here's an hypothetical function useMouse() {
let mouse = reactive({ x: 0, y: 0 });
window.addEventListener('mousemove', e => ({ x: mouse.x, y: mouse.y } = e));
return toRefs(mouse);
}
// Variation:
function useMouse() {
let x = ref(0), y = ref(0);
window.addEventListener('mousemove', e => ({x: x.value, y: y.value } = e));
return { x, y };
}
This is something that I disagree with and have argued against in other issues. I would rather that mixins return reactive objects rather than torefs. The main argument here is that people will surely do that: let { x, y } = useMouse(); and if I think it makes more sense to encourage people to return reactive objects rather than toRefs, and here's why:
|
Because The definition of function toRefs(object) {
if ((process.env.NODE_ENV !== 'production') && !isReactive(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`);
}
const ret = {};
for (const key in object) {
ret[key] = toProxyRef(object, key);
}
return ret;
}
function toProxyRef(object, key) {
return {
_isRef: true,
get value() {
return object[key];
},
set value(newVal) {
object[key] = newVal;
}
};
} |
First of all thanks for those detailed explanations. I appreciate it. Also, as I read the Vue 3 docs I suppose to know more or less how the things are about to work. But exactly this example with // useMousePosition.ts
export function useMousePosition() {
const pos = reactive({
x: 0,
y: 0
});
function updatePosition(event: MouseEvent) {
pos.x = event.pageX;
pos.y = event.pageY;
}
// other stuff like event handlers etc.
// edited: return pos;
return { pos }
}
// App.vue
setup() {
return {
...useMousePosition()
};
}; The above code works as it is. My problem was, that for the entire time I was thinking, that the values from the composable function should loose its reactivity as soon as the function is spreaded or destructured (as in the example above, where I did it in the return statement). I didn't know that the reactivity get lost only then if those operations would be made direct inside function's body and the new variables (copied values) would be returned. So the code below break the reactivity as intended: // App.vue
setup() {
// in this case reactivity will be lost (it needs to be wrapped with `toRefs`)
const { x, y } = useMousePosition();
return { x, y };
}; |
@kwiat1990 your example does NOT work: https://jsfiddle.net/yyx990803/bon6hu59/ Did you copy the snippet with |
Hi, i have edited my code - the |
@kwiat1990 then you are referencing it in your template as The problem with this approach is that any consumer of your function will have to remember to access the desired state as a nested property in the template. It's a viable style if you control all the code, but not as straightforward when your function is expected to be used by other developers. The point of |
Yeah, that's exactly how did it. I have always returned an object from the composable function and then accessed its properties in the component's template, e.g. To be honest I don't get it still, what's the difference. In "my" approach I return an object, which is the reference to the original object (the reactive one), while in the second case ( |
@kwiat1990 The important bit is that you access reactive data on a reactive receiver.
When you return an object wrapping the position If you return If you return the reactive position directly then spread it, it's the same case as the previous one but this time, |
Wow. Great job @jods4. Your breakdown should be a blog article for Vue users wondering about the "why of refs". If ever there was a conceptual understanding needed in the minds of Vue(3) developers, your explanation hits that nail on the head very well, and above and beyond the RFC itself. Thanks for the effort on my part and if you don't want to get it out in the wild as a blog article, would you mind me using it? And sorry everyone for sidetracking/ hijacking. Just need a quick response from @jods4, as I don't know how to contact him/her personally. Scott |
@smolinari of course, help yourself. |
You covered refs still in my mind, because you explained the need for Scott |
@smolinari you can find me on discord, jods # 2500 |
How about nested objects? function getUser() {
const user = reactive({
id: 339,
name: 'Fred',
age: 42,
education: {
physics: {
degree: 'Masters'
},
}
});
return {user};
} How do you recommend destructing this object so that the component receives and operates only two parameters: <div id="app">
{{name}} has a {{degree}} degree in physics
</div> |
@cawa-93 First you should define what behavior you expect exactly for the
Some solutions: const { name } = toRefs(user)
// This will always refer to the original `physics` object
const { degree } = toRefs(user.education.physics)
// This refers to the current `education.physics` object and will notify changes at any level
const degree = computed({
get: () => user.education.physics.degree,
set: v => user.education.physics.degree = v
})
// If you want to put them both in the same object, you can also go with this:
const state = markNonReactive({
get name() { return user.name },
set name(v) { user.name = v },
get degree() { return user.education.physics.degree },
set degree(v) { user.education.physics.degree = v },
}) Should we get const { name, degree } = extractRefs(user, u => { name: u.name, degree: u.education.physics.degree }) Final note: all this code creates writable refs. const data = readonly({
get name() { return user.name },
get degree() { return user.education.physics.degree },
}) Bonus chatter: why cc: @LinusBorg I think you'll find these example interesting. |
Hi,
I've read that if something is defined using
reactive()
and then spreaded or destructured, it won't be reactive anymore. And that's whytoRefs()
need to be used while spreading/destructuring.On Vue 3 Composition API Page there is an example with some exaplanation. So far, so good. I have played with it and can't understand how lack of
toRefs
could possibly break this code. I mean, it's not very obvious how/why/when the things can break. Every instance of a composable function has its own scope etc.The code below is so far the only example of lost reactivity I could get. Though it' s pretty obvious and simple to detect that without
toRefs
it's doesn't work as intended:Could someone please explain me why it is important that a composable function returns data defined inside
reactive
wrapped intoRefs
in order to retain reactivity? And what could an example of this look like?The text was updated successfully, but these errors were encountered: