vue.jsvuejs3vue-componentvue-composition-apivue-props

Passing object with reactive properties as a prop triggers child component onUpdated hook when updating parent component state


Consider a simple component Comp.vue file that only logs a message in the console when updated:

// Comp.vue

<script setup>
  import { onUpdated } from 'vue'
  onUpdated(() => {
    console.log('updated component')
  })
</script>
<template></template>

An App.vue file mounts Comp.vue and also logs a message in its onUpdated hook:

// App.vue

<script setup>
  import Comp from './Comp.vue'
  import { ref, onUpdated } from 'vue'

  onUpdated(() => {
    console.log('updated parent')
  })

  const text = ref('')
  const data = ref()
</script>

<template>
  <input v-model="text"/>
  <Comp/>
</template>

There's a text ref bound to an input element to trigger onUpdated on the parent. At this point, writing on the text input only logs 'updated parent' in the console.

If an object with a reactive property is passed to Comp as a prop, writing on the input text logs both 'updated component' and 'updated parent' in the console:

<Comp :data={ data }/>

That is, Comp's onUpdated hook is being triggered with changes to App's state. Here's a demo.

To avoid this, a computed property could be passed instead:

const computedData = computed(() => ({ data: data.value }))
// ...
<Comp :data="computedData"/>

However, I'd like to know why passing { data } directly causes Comp onUpdated hook to be triggered when text is updated.


Solution

  • When you use a ref in a template, and change the ref's value later, Vue automatically detects the change and updates the DOM accordingly... When a component is rendered for the first time, Vue tracks every ref that was used during the render. Later on, when a ref is mutated, it will trigger a re-render for components that are tracking it. [1]

    I take this to mean that when the text ref is updated, the DOM of the component tracking it, i.e. App, is re-rendered. This re-computes, if necessary, the data prop passed to Comp. Then, in the situation described in the question, as Estus Flask pointed out, the recomputed prop results in a new object, different to the previous one passed, causing the Comp state to be updated.

    However, if the plain object does not reference variables declared in the script section, the data prop is not recomputed.

    <!-- Does not update when input changes -->
    <Comp :data="{ data: 2 }"/>
    

    The prop is only recomputed if the passed object references variables declared on the script section. As mentioned in the question, if the object includes reactive data, a computed property may instead be used. If there's no reactive data, the object should be defined in the script section and then passed as a prop.

    <script setup>
    const reactiveData = ref(0)
    const regularData = 0
    const computedProp = computed(() => ({ data: reactiveData.value }))
    const staticProp = { data: regularData }
    </script>
    
    <tempalte>
        <!-- Do not update when input changes -->
        <Comp :data="computedProp"/>
        <Comp :data="staticProp"/>
    </template>
    

    See full example.

    [1] Why Refs?, Vue.js