Search code examples
javascriptvue.jsvuejs2v-model

How to modify a 'value' prop in VueJS before `$emit('input')` finishes updating it


I have a question about creating VueJS components that are usable with v-model which utilise underlying value prop and $emit('input', newVal).

props: {
  value: Array
},
methods: {
  moveIdToIndex (id, newIndex) {
    const newArrayHead = this.value
      .slice(0, newIndex)
      .filter(_id => _id !== id)
    const newArrayTail = this.value
      .slice(newIndex)
      .filter(_id => _id !== id)
    const newArray = [...newArrayHead, id, ...newArrayTail]
    return this.updateArray(newArray)
  },
  updateArray (newArray) {
    this.$emit('input', newArray)
  }
}

In the above code sample, if I do two modifications in quick succession, they will both be executed onto the "old array" (the non-modified value prop).

moveIdToIndex('a', 4)
moveIdToIndex('b', 2)

In other words, I need to wait for the value to be updated via the $emit('input') in order for the second call to moveIdToIndex to use that already modified array.

Bad solution 1

One workaround is changing updateArray to:

  updateArray (newArray) {
    return new Promise((resolve, reject) => {
      this.$emit('input', newArray)
      this.$nextTick(resolve)
    })
  }

and execute like so:

await moveIdToIndex('a', 4)
moveIdToIndex('b', 2)

But I do not want to do this, because I need to execute this action on an array of Ids and move them all to different locations at the same time. And awaiting would greatly reduce performance.

Bad solution 2

A much better solution I found is to just do this:

  updateArray (newArray) {
    this.value = newArray
    this.$emit('input', newArray)
  }

Then I don't need to wait for the $emit to complete at all.

However, in this case, VueJS gives a console error:

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"

Does anyone have any better solution?


Solution

  • OK. These are your options as far as I understand your use case and application.

    First of all, don't mutate the props directly save the props internally and then modify that value.

    props: {
      value: Array
    },
    data() {
      return {
        val: this.value
      }
    }
    

    If the next modification to the array is dependent on the previous modification to the array you can't perform them simultaneously. But you need it to happen fairly quickly ( i will assume that you want the user to feel that it's happening quickly ). What you can do is perform the modification on the val inside the component and not make it dependent on the prop. The val variable is only initialized when the component is mounted. This way you can modify the data instantly in the UI and let the database update in the background.

    In other words, your complete solution would look like this:

    props: {
      value: Array
    },
    data () {
      return {val: this.value}
    },
    methods: {
      moveIdToIndex (id, newIndex) {
        const newArrayHead = this.val
          .slice(0, newIndex)
          .filter(_id => _id !== id)
        const newArrayTail = this.val
          .slice(newIndex)
          .filter(_id => _id !== id)
        const newArray = [...newArrayHead, id, ...newArrayTail]
        return this.updateArray(newArray)
      },
      updateArray (newArray) {
        this.val = newArray
        this.$emit('input', newArray)
      }
    }
    

    This solution fixes your problem and allows you to execute moveIdToIndex in quick succession without having to await anything.

    Now if the array is used in many places in the application next best thing would be to move it to a store and use it as a single point of truth and update that and use that to update your component. Your state will update quickly not simultaneously and then defer the update to the database for a suitable time.