Home > Software engineering >  Why does an internal value react to changes from the whole ref?
Why does an internal value react to changes from the whole ref?

Time:12-07

I have an object ...

export class Note {
  type = ''
  id: string = v4()
  creationDate: string = DateTime.now().toISO()
  modificationDate: string = DateTime.now().toISO()
  text = ''
  title = ''
  tags: string[] = []
  deleted = false
  deletedAt?: string = ''
  erased = false
  erasedAt?: string = ''
  archived = false
  archivedAt?: string = ''
  deleteOn?: string = ''
  pinned = false
  // methods follow
}

... that I pass as a prop to a component

<script lang='ts' setup>

const props = defineProps({
  currentNote: {type: Object, default: () => {return {}}},
})

const note = toRef(props, 'currentNote') as Ref<Note>

const toWatch = computed(() => [toRef(note, 'title'), toRef(note, 'text'), toRef(note, 'tags'), toRef(note, 'type')])
watch(toWatch, debounce(() => {console.log(JSON.stringify(note)); note.value.writeToOffline()}, 1000), {deep: true})

(...)

What happens, however, is that the watch callback gets triggered whenever any property of note changes.

The construction above was to only watch selected properties, but apparently they carry any change.

What is wrong with the logic in my code?

CodePudding user response:

Not reproducible. Check demo below...

  1. Changes to archived do not trigger watcher
  2. Changes to title also do not trigger watcher - note the code inside toWatch computed - the code is same as in your example: Vue.toRef(note, 'title')
  3. Changes to text triggers the watcher - To fix problem above, you need to use Vue.toRef(note.value, 'text') eg. to pass a ref value instead of ref itself to toRefs

All this works without {deep: true} because watch can watch an array of refs (and as it seems also ref containing array of refs)

class Note {
    constructor() {
        this.type = '';
        this.id = 'asdsad';
        this.text = 'text';
        this.title = 'title';
        this.tags = [];
        this.deleted = false;
        this.deletedAt = '';
        this.erased = false;
        this.erasedAt = '';
        this.archived = false;
        this.archivedAt = '';
        this.deleteOn = '';
        this.pinned = false;
    }
}

const app = Vue.createApp({
  setup(){
    const note = Vue.reactive(new Note())
    
    const modifyArchived = function() {
      note.archived = !note.archived
    }
    const modifyTitle = function() {
      note.title = note.title   ' '
    }
    const modifyText = function() {
      note.text = note.text   ' '
    }

    return {
      note,
      modifyArchived,
      modifyTitle,
      modifyText
    }
  },
})

app.component('child', {
  props: ['currentNote'],
  setup(props) {
    const note = Vue.toRef(props, 'currentNote')
    const changes = Vue.ref(0)

    const toWatch = Vue.computed(() => [Vue.toRef(note, 'title'), Vue.toRef(note.value, 'text'), Vue.toRef(note, 'tags'), Vue.toRef(note, 'type')])
    Vue.watch(toWatch, () => { changes.value   } /*, {deep: true} */)

    return { 
      note,
      changes
    }
  },
  template: `
    <div>Child: (changes = {{ changes }})</div>
    <div>archived: {{ note.archived }}</div>
    <div>title: {{ note.title }}</div>
    <div>text: {{ note.text }}</div>    
    `
})
app.mount('#app')
<script src="https://unpkg.com/[email protected]/dist/vue.global.js"></script>
<div id='app'>
  <div>Parent:</div>
  <div><button @click="modifyArchived">Modify archived (no trigger)</button></div>
  <div><button @click="modifyTitle">Modify title (no trigger - not ok!)</button></div>
  <div><button @click="modifyText">Modify text (trigger - ok!)</button></div>
  
  <child :current-note="note"></child>
</div>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

  • Related