Skip to content
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

Open
kwiat1990 opened this issue Mar 21, 2020 · 14 comments

Comments

@kwiat1990
Copy link

kwiat1990 commented Mar 21, 2020

Hi,

I've read that if something is defined usingreactive() and then spreaded or destructured, it won't be reactive anymore. And that's why toRefs() 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:

    const foo = reactive({ bar: "" });
    const { buz } = foo;
    foo.bar = "lorem ipsum";
    console.log(foo.bar, bar); // => "lorem ipsum", ""

Could someone please explain me why it is important that a composable function returns data defined inside reactive wrapped in toRefs in order to retain reactivity? And what could an example of this look like?

@kwiat1990 kwiat1990 changed the title When it's really needed to used toRefs in order to retain reactivity of reactive value When it's really needed to use toRefs in order to retain reactivity of reactive value Mar 21, 2020
@jods4
Copy link

jods4 commented Mar 21, 2020

Consider an object that tracks the mouse position: mouse = { x, y }.
Let's forget Vue and reactive for a sec' and look at regular JS code:

f(mouse) {
  let x = mouse.x;
  // do stuff with x
}

You don't expect the x variable to change whenever mouse is updated, nor do you expect mouse.x to change whenever you assign x a new value.
Of course not, because x is a local variable holding a copy of the value of mouse.x at the time of assignment.

Also note that destructuring is not magical, it's syntactic sugar for assigning variables.
So let { x } = mouse is basically the same thing as let x = mouse.x.

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. mouse.x. If you copy the value into a variable then it's just a number and there will be no reactivity when using the variable.

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 toRefs does: it creates ref that delegate to the original object and can be passed around as a single reactive value.

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.
Your component props is a reactive object, so here's an example:

// 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 len(props.name) would not work. len would receive a static string, which is the value of props.name at the time of setup. It would never update.

And what could an example of this look like?

Here's an hypothetical useMouse in this style:

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 };
}

Could someone please explain me why it is important that a composable function returns data defined inside reactive wrapped in toRefs in order to retain reactivity?

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 useMouse didn't return torefs then user code will most likely not work as they expect (everything would be static and non-reactive).

I think it makes more sense to encourage people to return reactive objects rather than toRefs, and here's why:

  1. People need to be aware of the returned types anyway. From my example above, if you do x + y it's broken code, you have to keep in mind that it's refs and do x.value + y.value.

  2. Doing .value everywhere is really not any better than doing mouse.x and mouse.y in the first place. The latter has less foreign concepts/ceremony.

  3. People have to understand that extracting / copying a value from a reactive object is a static copy. They really, really have to understand that otherwise they'll have tons of bugs in many other places. From their own reactive objects, in the first place (and see 4). The more you try to make stuff magically work for them, the less they'll learn, the more problems they'll have later.

  4. This is inconsistent with Vue itself, as props is a reactive object, not an objects of refs. So if people do let { x, y } = props they're wrong.

  5. If you don't destructure the reactive object (you keep mouse around), you can put it in your returned state and never need to .value in your view, no problem. There has been some back-and-forth caused by automatically unwrapping refs in the view and it mostly work. "Mostly" being key here, as it doesn't work for stuff coming out of a computed or inside an array, for example.

  6. If you really want to destructure, or reshape a reactive object before putting it in your state, or you want to extract one property out of it... go and use toRefs, I'm not saying it's bad!

  7. Nit: if you don't toRefs you allocate less objects.

@WJCHumble
Copy link

WJCHumble commented Mar 21, 2020

Because reactive API is based on ES2015 Proxy . When you define a object by Proxy,and you use spread or destructuring for it, it will lose reference to object. So, vue3.0 use toRefs to proxy the reactive object, and then, you can use spread or destructuing which also keep reference to reactive object.

The definition of toRefs in source code:

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;
        }
    };
}

@kwiat1990
Copy link
Author

kwiat1990 commented Mar 21, 2020

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, which is a simple composable function, made me think. So I prepared a small app in which I copy-pasted the code from docs:

// 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 };
};

@yyx990803
Copy link
Member

@kwiat1990 your example does NOT work: https://jsfiddle.net/yyx990803/bon6hu59/

Did you copy the snippet with toRefs instead?

@kwiat1990
Copy link
Author

kwiat1990 commented Mar 21, 2020

@kwiat1990 your example does NOT work: https://jsfiddle.net/yyx990803/bon6hu59/

Did you copy the snippet with toRefs instead?

Hi, i have edited my code - the useMousePosition function should return an object: return { pos } instead of return pos. Then my example works.

@yyx990803
Copy link
Member

yyx990803 commented Mar 21, 2020

@kwiat1990 then you are referencing it in your template as pos.x and pos.y, right? The reactivity is about the connection between x/y and pos, not between pos and the object you returned. So if you do return { ...useMouse().pos } it would break again.

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 toRefs is to contain the complexity within the composition functions so consumers of these functions don't need to be thinking about usage restrictions.

@kwiat1990
Copy link
Author

@kwiat1990 then you are referencing it in your template as pos.x and pos.y, right? The reactivity is about the connection between x/y and pos, not between pos and the object you returned. So if you do return { ...useMouse().pos } it would break again.

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. {{ pos.x }}, {{ pos.y }}.

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 (return pos) it's the value itself to be returned?

@jods4
Copy link

jods4 commented Mar 21, 2020

@kwiat1990 The important bit is that you access reactive data on a reactive receiver.

pos is a reactive object, so pos.x is fine.

When you return an object wrapping the position { pos } and then spread it: return { ...useMouse() }
it is equivalent to: return { pos: useMouse().pos }
pos is still the reactive object, accessing pos.x is still working.

If you return toRefs(pos), then spread it return { ...useMouse() }
it is equivalent to mouse = useMouse(); return { x: mouse.x, y: mouse.y }.
Because of toRefs, in this case x an y are not plain values (numbers) but refs, which are reactive.
In your setup, you'll need to access them through x.value. In the views, because of some Vue magic, you should use them directly: {{ x }} but it's really as if you did x.value.
All of this is reactive.

If you return the reactive position directly then spread it, it's the same case as the previous one but this time, x and y won't be refs, they will just be plain numbers copied from pos properties.
You use them as x and y in both your setup and the view and none of this will be reactive.

@smolinari
Copy link
Contributor

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

@jods4
Copy link

jods4 commented Mar 22, 2020

@smolinari of course, help yourself.
If this was a reactivity explainer, I feel like I skipped over "what is a ref / why do we need it" and went straight to toRefs.

@smolinari
Copy link
Contributor

You covered refs still in my mind, because you explained the need for .value via explaining toRefs. That's what also caught my eye. Is there a channel I can contact you personally, once the blog article is written?

Scott

@jods4
Copy link

jods4 commented Mar 25, 2020

@smolinari you can find me on discord, jods # 2500

@cawa-93
Copy link

cawa-93 commented Mar 25, 2020

How about nested objects?
Suppose I get some reactive user object from a third party function:

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: name and degree in physics?

<div id="app">
  {{name}} has a {{degree}} degree in physics
</div>

@jods4
Copy link

jods4 commented Mar 25, 2020

@cawa-93 First you should define what behavior you expect exactly for the degree.
If I reassign the education property, do you expect:

  • degree to refer to to stil refer to the original object?
  • degree to refer to the newly assigned object?
  • in the latter case did you expect a reactive trigger / update?

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 markReactive and markRef (see #129 (comment)), you could come up with a lot of other helpers to create two refs in one go.
You could build a function that performs this:

const { name, degree } = extractRefs(user, u => { name: u.name, degree: u.education.physics.degree })

Final note: all this code creates writable refs.
If you only want readonly data to bind in a view (such as your example), just go with this:

const data = readonly({
  get name() { return user.name },
  get degree() { return user.education.physics.degree },
})

Bonus chatter: why markNonReactive, readonly?
Anything you put in a reactive object graph or pass to the view is deeply wrapped into reactive proxies automatically.
You can leave it out and it'll work the same, but it can create identity issues if at one point you put that object in a reactive graph as you'll end up with one proxified instance and one non-proxified (and they're not equal).
Current best practice discussed in github is to always wrap your state in a reactive primitive, whether reactive, readonly, or not, as it'll guarantee you always work with the proxified object and won't have 2 references for the same instance.


cc: @LinusBorg I think you'll find these example interesting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants