Home » vue » Vue computed property has not been updated although the array from which it depends has been explicitly reassigned

Vue computed property has not been updated although the array from which it depends has been explicitly reassigned

Posted by: admin November 26, 2021 Leave a comment

Questions:

From the answer on question What is affecting on will Vue computed property re-computed or no? I knew that

Vue cannot detect the following changes to an array:

When you directly set an item with the index, e.g.
vm.items[indexOfItem] = newValue When you modify the length of the
array, e.g. vm.items.length = newLength

Here is definitely other case because I explicitly assigning the new value to array.

Target

  1. Images slider must display currently selected photo
  2. When photosURLs array is empty, slider should display dummy image (like "no image") instead.
  3. When product.photosURLs updated, slider must display updated photo (or dummy, if all photos has been deleted).

Implementation

Below implementation reaches to first and second target.

Markup (pug)

.ImageSliderForNarrowScreens
  .ImageSliderForNarrowScreens-ActiveImage-DefaultBackgroundUnderlay

    .ImageSliderForNarrowScreens-ActiveImage-ImageLayer(
      v-if="product.photosURLs.length < 2"
      :style="`background-image: url(${activeProductPhotoURL_ForImagesViewer});`"
    )

    template(v-else)
      .ImageSliderForNarrowScreens-ActiveImage-ImageLayer(
        :key="'IMAGE_SLIDER-IMAGE-'+activeProductPhotoArrayIndexForImagesViewer"
        :style="`background-image: url(${activeProductPhotoURL_ForImagesViewer});`"
        v-touch:swipe.left="switchImagesSliderToPreviousPhoto"
        v-touch:swipe.right="switchImagesSliderToNextPhoto"
      )

Logic (TypeScript)

Images slider refers to product.photosURLs.

  • When user switched to editing mode, product.photosURLs will be copy to uploadedProductPhotosURLs, the array of updated but not submitted yet photos URLs.
  • When uploadedProductPhotosURLs will be submitted, uploadedProductPhotosURLs will be copy to product.photosURLs;
import { Vue, Component, Watch } from "vue-property-decorator";


@Component
export default class MyComponent extends Vue {

  private product!: Product;
  private uploadedProductPhotosURLs: Array<string> = [];

  /* --- Product photos viewing ------------------------------------------------------------------------------------ */
  private activeProductPhotoArrayIndexForImagesViewer: number = 0;

  private get activeProductPhotoURL_ForImagesViewer(): string {
    if (this.product.photosURLs.length === 0) {
      return PRODUCT_DUMMY_PHOTO_URL;
    }
    return this.product.photosURLs[this.activeProductPhotoArrayIndexForImagesViewer];
  }

  // --- Well, it does not matter, I just mentioned it in template
  private switchImagesViewersToNextProductPhoto(): void {
    this.activeProductPhotoArrayIndexForImagesViewer =
      this.activeProductPhotoArrayIndexForImagesViewer !== this.product.photosURLs.length - 1 ?
        this.activeProductPhotoArrayIndexForImagesViewer + 1 : 0;
  }

  private switchImagesViewersToPreviousProductPhoto(): void {
    this.activeProductPhotoArrayIndexForImagesViewer =
      this.activeProductPhotoArrayIndexForImagesViewer !== 0 ?
        this.activeProductPhotoArrayIndexForImagesViewer - 1 : this.product.photosURLs.length - 1;
  }
  // ------------------------------------------------------------


  /* --- Submitting of changes ------------------------------------------------------------------------------------ */
  private async onClickSaveProductButton(): Promise<void> {

    try {

      await ProductsUpdatingAPI.submit({
        // ...
        productPhotosURLs: this.uploadedProductPhotosURLs
      });


      console.log("checkpoint");
      console.log("uploadedProductPhotosURLs:");
      console.log(this.uploadedProductPhotosURLs);

      console.log("'product.photosURLs' before updating:");
      console.log(JSON.stringify(this.product.photosURLs, null, 2));

      this.product.photosURLs = this.uploadedProductPhotosURLs;
      // normally, I must set it to 0, but for now it does not affect
      // this.activeProductPhotoArrayIndexForImagesViewer = 0;
      console.log("'product.photosURLs' after updating:");
      console.log(JSON.stringify(this.product.photosURLs, null, 2));

    } catch (error) {

      // ....
    }
  }
}

Problem

When this.product.photosURLs is empty, let’s try to add and submit new photos. The debug output will be:

checkpoint
uploadedProductPhotosURLs:
[
  "https://XXX/c732d006-1261-403a-a32f-f73c0f205aa8.jpeg", 
  "https://XXX/2b7ae2e2-4424-4038-acee-9624d5b937bc.jpeg", 
   __ob__: Observer
]
'product.photosURLs' before updating:
[]
'product.photosURLs' after updating:
[
  "https://XXX/c732d006-1261-403a-a32f-f73c0f205aa8.jpeg",
  "https://XXX/2b7ae2e2-4424-4038-acee-9624d5b937bc.jpeg"
]

From the view point of algorithm, all correct, but computed property activeProductPhotoURL_ForImagesViewer (getter in TypeScript OOP syntax) has not been recomputed!
It means, PRODUCT_DUMMY_PHOTO_URL is still displaying.

private get activeProductPhotoURL_ForImagesViewer(): string {
  if (this.product.photosURLs.length === 0) {
    return PRODUCT_DUMMY_PHOTO_URL;
  }
  return this.product.photosURLs[this.activeProductPhotoArrayIndexForImagesViewer];
}

Same if to delete images: debug output is matching to expected, but cached image that has been deleted is still displaying!

checkpoint
uploadedProductPhotosURLs:
[__ob__: Observer]
'product.photosURLs' before updating:
[
  "https://XXX/c732d006-1261-403a-a32f-f73c0f205aa8.jpeg",
  "https://XXX/2b7ae2e2-4424-4038-acee-9624d5b937bc.jpeg"
]
'product.photosURLs' after updating:
[]

Same if to replace images.

Answers:

Extending on @Estradiaz’s comment, and as mentioned in Vue’s own documentation: Reactivity in Depth,

Vue cannot detect property addition or deletion.

you either need to initialize product.photosURLs maybe with an empty array, or else you need to use Vue.set() in order to assign value to this.product.photosURLs in your onClickSaveProductButton function, like this:

Vue.set(this.product, 'photosURLs', this.uploadedProductPhotosURLs);