While playing with Vue 3 reactivity I incountred a behaviour that I couldn't explain.
I created a ref
of a primitive. Checking isRef
on it returns obviously true
. But when passed to a child component as a prop
, isRef()
and also isReactive()
return false
. Why ?
Also, even if they both return false
, the watcher of this prop I added in the child component is triggered if the value changes. How could it trigger if the watched value is not a ref
nor reactive
?
Here is a code snippet for both the parent and the child :
Parent.vue
let foo = ref(0);
function changeFoo() {
foo.value ;
}
console.log("parent check is reactive?", isReactive(foo)); // retruns false
console.log("parent check is ref?", isRef(foo)); // retruns true
watch(foo, (value, oldValue) => {
console.log("foo changing from", oldValue, "to ", value);
});
Child.vue
const props = defineProps<{
foo: number;
}>();
console.log("child check is reactive?",isReactive(props), isReactive(props.foo)); // returns true false
console.log("child check is ref ?",isRef(props), isRef(props.foo)); // returns false false // Question 1: Why props.foo is not a Ref ?
watch(props, (value, oldValue) => {
console.log("props changing from", oldValue, " to ", value);
});
watch(()=>props.foo, (value, oldValue) => {
// Question 2: Why this Watcher detects changes of "foo" without it being a ref nor reactive ?
console.log("props.foo changing from ", oldValue, " to ", value);
});
And a link to Vue SFC Playground here
Bonus question :
When foo
is passed to a composable used in the child component, the watcher inside the composable is not triggered unless we pass foo
via a toRef but we don't need this additional step if foo
is a ref/reactive object. Why primitives needs this addition step ?
CodePudding user response:
But when passed to a child component as a prop, isRef() and also isReactive() return false.
You're only telling half the story here, as your own comment actually indicates:
console.log("child check is reactive?",isReactive(props), isReactive(props.foo)); // returns true false
isReactive(props)
does return true, because the entire props
object (which wraps foo
) is reactive. Any update the parent makes to foo
, gets passed down to the Child as an updated props
object. It's true props.foo
is not a ref/reactive, because it doesn't need to be. As long as props
is reactive, props.foo
will update
The watcher is able to activate on changes to props.foo
because you're actually using special syntax where you pass in a "getter" to the watch specifically meant for watching the property of a reactive object (in your case: props). There's an example in the Vue docs that says the same thing.
If you ever needed to assign props.foo
to it's own reactive variable, say to pass to a composable, that's where toRef can be used.
// reactive but not synced with props.foo
const newFoo = ref(props.foo)
// reactive AND synced with props.foo
const newFoo = toRef(props, 'foo')
As I indicated with the above comments, if you make a new variable with just ref
, it'll be reactive, but it won't be synced with it's source property, i.e. the first newFoo
won't reactively update when props.foo
updates, but the second newFoo
will. This is also explained well in the Vue docs