Subscribe on changes!

Reactivity retained for ref(props.array)

avatar
Aug 31st 2020

Version

3.0.0-rc.9

Reproduction link

https://codesandbox.io/s/broken-pine-spe90?file=/src/Child.vue

Non-read-only repo: https://github.com/nerdcave/vue-3-ref-test Simpler: https://github.com/nerdcave/v3-array-ref2

Steps to reproduce

  • Click "inc count" and observe the countPlus1 computed does not trigger.
  • Click "add items" and observe the itemsSorted computed does trigger.

What is expected?

I expect that ref(props.items) should not retain reactivity and that I need toRef(), as I would for a primitive.

What is actually happening?

ref(props.count) doesn't retain reactivity, but ref(props.items) does. Clicking "add items" updates the "composed computeds" array, unexpectedly.


Side effect of objects being reactive()'d when assigned to ref values? Not sure if it's a bug, gotcha, or expected behavior, but it definitely threw me off.

avatar
Aug 31st 2020

This is probably because items is an array and passed by reference. Please don't provide read-only repros

avatar
Aug 31st 2020

Added a public repo that can be pulled down.

I wasn't convinced it was simply a pass by reference issue because count and items appear to have different ref internals. e.g. ref(props.items) contains Proxy objects and ref(props.count) does not:

image

avatar
Aug 31st 2020

if ref() wraps an object, it will make this object reactive

avatar
Aug 31st 2020

@unbyte Understood, but should changes to the original still propagate all the way through? That seems unexpected. Added a simpler repo link that demonstrates.

avatar
Aug 31st 2020

This is expected behavior.

avatar
Aug 31st 2020

Would this be considered a "gotcha" to any degree or fully and predictably expected? I've chosen to use toRef() over ref() in these situations because it feels like it conveys the intent of the code much more accurately, but I'm not even sure if that's recommended.

avatar
Sep 1st 2020

Should a shallowRef suit your case?

From ref documentation:

If an object is assigned as a ref's value, the object is made deeply reactive by the reactive method.

From shallowRef documentation:

Creates a ref that tracks its own .value mutation but doesn't make its value reactive.

I wouldn't consider it a "gotcha", as the deep reactivity is, usually, what you want for your Vue model. See Vue.set and Vue.delete caveats in 2.x.

avatar
Sep 1st 2020

It's not that I need to prevent it from happening, necessarily, it's more about making sure it's not some bug or gotcha that should be further documented I guess? This specific scenario just struck me as a bit unintuitive.

avatar
Sep 7th 2020

@yyx990803 , For me it is a bug. The doc clearly explains that props are One-Way Data Flow

All props form a one-way-down binding between the child property and the parent one: when the parent property updates, it will flow down to the child, but not the other way around. This prevents child components from accidentally mutating the parent's state, which can make your app's data flow harder to understand.

The problem I have is that I have a parent component which opens a dialog (child) with a form. The changes in the array in the child component should only update the parent when the form is submitted. Now it updates the the value of the array in the parent even when the form of the child is not submitted.

I don't see how I can break the reactivity to avoid updates from the child to the parent. shallowRef doesn't seem to change anything.

Can you maybe reconsider to reopen this issue?

thx, FIlip.

avatar
Sep 7th 2020

@bzd2000 note your issue is different from the original one. It's how Vue reactivity has always worked. If you want to explicitly prevent the child from mutating the state you passed down you can use readonly: https://v3.vuejs.org/api/basic-reactivity.html#readonly

But again, your use case sounds like wrong design because you are allowing the child to mutate the state yet expect the state to not trigger updates. That's not what one-way data flow is about.

avatar
Sep 8th 2020

hi @yyx990803 ,

That it is indeed what I want to achieve. So maybe misunderstood the props then.

when I update the string in the child, the string in the parent is not updated. when I update the array in the child, the array in the parent is updated. I would expect that both props behave in the same way (meaning one way binding from parent to child only)

export default defineComponent({
  name: "child",
  components: {},
  directives: {},
  props: {
    modelValue: { type: Array, required: true},
    name: {type: String, required: true}
  },
  setup(props, context) {
    const localArray: Ref<string[]> = ref(props.modelValue);
    const localString: Ref<string> = ref(props.name);
    
    const addItem = () => {
      localArray.value.push("new item");
    };

    const updateString = () => {
      localString.value = "newString"
    };
  }
});

I fixed it by const localArray = ref([...props.modelValue]);

Maybe I don't understand the one-way data flow then, but if a read the doc it says the updates of props go from the parent to the child and not the other way around.
But that is what happing for arrays right?
Although I understand it is because of the array passed as a reference, still conceptually it feels wrong and confusing.