Reactivity retained for ref(props.array)
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.
This is probably because items
is an array and passed by reference. Please don't provide read-only repros
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:
@unbyte Understood, but should changes to the original still propagate all the way through? That seems unexpected. Added a simpler repo link that demonstrates.
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.
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.
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.
@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.
@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.
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.