vue.jsvuejs3vue-composition-apivue-reactivity

Vue 3 get delta object of reactive updates


Consider a reactive object (or array) that is dynamically populated with data:

import { reactive, computed } from 'vue'

const entries = reactive({}) // global state beyond Template level
const docs = [
  { id: 1, name: 'foo', value: 'bar' },
  { id: 2, name: 'bar', value: 'moo' }
]

Object.entries(docs).forEach(doc => {
  entries[doc.id] = doc
})


export const useEntries = () => {
  const getAll = computed(() => Object.values(entries))
  const update = (id, newValue) => {
    entries[id].value = newValue
  }
  const changes = () => { /* ??? */ }

  return { getAll, update, changes } 
}

For performance reason, I want a Templates to watch only updated entries and not iterate the entire list, if the update is triggered within another Template.

Template A:

<script setup>
import { useEntries } from '/path/to/useEntries'

const { update } = useEntries()
</script>
<template>
  <button @click="update(1, 'moo)">Make moo</button>
</template>

Template B:

<script setup>
import { useEntries } from '/path/to/useEntries'
import { watch } from 'vue'

const { changes } = useEntries()

watch(changes, (entries) => {
  // process only updated entries here
})
</script>

Note, in this example I update only one entry, but it also might be 50 at once in a list of hundreds. I'd like to avoid a second list to manage.


Solution

  • you can manage "delta" object that holds changes you store entry’s id in the changes object then add changes function that returns only entries that have been updated

    import { reactive, computed } from 'vue'
    
    const entries = reactive({})  
    const updatedEntries = reactive(new Set()) 
    
    const docs = [
      { id: 1, name: 'foo', value: 'bar' },
      { id: 2, name: 'bar', value: 'moo' }
    ]
    
    docs.forEach(doc => {
      entries[doc.id] = doc
    })
    
    export const useEntries = () => {
      const getAll = computed(() => Object.values(entries))
    
      const update = (id, newValue) => {
        entries[id].value = newValue
        updatedEntries.add(id) 
      }
    
      const changes = computed(() => {
        const changed = {}
        updatedEntries.forEach(id => {
          changed[id] = entries[id]
        })
        return changed
      })
    
      return { getAll, update, changes }
    }
    

    Template A

    <template>
      <button @click="update(1, 'moo')">Make moo</button>
    </template>
    
    <script setup>
    import { useEntries } from '/path/to/useEntries'
    
    const { update } = useEntries()
    </script>
    

    Template B

    <template>
      <div v-for="entry in changedEntries" :key="entry.id">
        {{ entry.name }}: {{ entry.value }}
      </div>
    
    </template>
    
    <script setup>
    import { useEntries } from '/path/to/useEntries'
    import { watch, ref } from 'vue'
    
    const { changes } = useEntries()
    const changedEntries = ref({})
    
    watch(changes, (newChanges) => {
      changedEntries.value = newChanges
    })
    </script>