Home > Net >  Vue Array Class Proxy Not Reacting
Vue Array Class Proxy Not Reacting

Time:05-02

I have a class that extends Array, and as part of it, I want to intercept changes that are made to its properties, so I use Proxy, which is what I return from its constructor. It works just fine until I try to use it in my Vue component. See this example.

When the page first loads, you'll see the console log for Collection 1 in the watchEffect, which is the expected result. Then when you click the Add Filter button, you'll see that the display doesn't update and the watchEffect doesn't fire... expectation is that we'd get the console log like when the page loaded. However, if you inspect collection1, you'll see that the value was added.

Does anyone know why this doesn't work and how I can fix it? It feels like maybe my proxy is being tripped up with Vue's proxy wrapper, but I don't know enough about the internals to say that confidently.

Collection.js

export class MyCollection extends Array {
  constructor(data) {
    super();
    this.add(data);
    return new Proxy(this, {
      set(target, prop, value) {
        target[prop] = value;
        if (prop === 'filters') {
          const add = []
          target.records.forEach((item) => {
            if (item.id === target.filters) {
              add.push(item)
            }
          })
          target.add(add);
        }
        return true;
      }
    })
  }
  
  addFilters() {
    this.filters = 1
  }
  
  add(items) {
    this.length = 0;
    items = Array.isArray(items) ? items : [items];
    this.records = items;
    console.log('here', this.records, this);
    items.forEach((item) => this.push(item))
  }
}

App.vue

<script setup>
  import {watchEffect, computed, ref, toRaw} from "vue";
  import {MyCollection} from "./Collection.js";
  
  const collection1 = $ref(new MyCollection([{id: 1, display: 'one'}, {id: 2, display: 'two'}]));
  
  watchEffect(() => {
    console.log("wow", collection1);
  });
  
  const onClickUpdate1 =() =>  {
    collection1.addFilters();
  }
</script>

<template>
  <div>
    Collection 1
    <button @click='onClickUpdate1'>
      Add Filter
    </button>
  </div>
  <div v-for="item in collection1" :key="item.id">
    {{item.display}}
  </div>
</template>

CodePudding user response:

Try this edit I made to your code.

I have changed a few things:

  1. changed to script setup for better readability
  2. changed computed to reactive using the $red syntax from reactivity transform
  3. You were adding items with name: 'three' and displaying item.display. I changed that bit to add with display: 'three'.

It works now and I suspect the difference is in having changed from computed to reactive though I'm not sure. I'm going to read about it a bit more and update the answer accordingly.

CodePudding user response:

I think I found a solution, but I may have also found a bug in Vue, which I've reported. What I had to change was calling the receiver's method instead of the target's method in the set trap of MyCollection. Fiddle

MyCollection.js

export class MyCollection extends Array {
  constructor(data) {
    super();
    this.add(data);
    return new Proxy(this, {
      set(target, prop, value, receiver) {
        target[prop] = value;
        if (prop === 'filters') {
          const add = []
          target.records.forEach((item) => {
            if (item.id === target.filters) {
              add.push(item)
            }
          })
          // IMPORTANT: Had to use receiver here instead of target
          receiver.add(add);
        }
        return true;
      }
    })
  }
  
  addFilters() {
    this.filters = 1
  }
  
  add(items) {
    this.length = 0;
    items = Array.isArray(items) ? items : [items];
    this.records = items;
    items.forEach((item) => this.push(item))
  }
}

The second issue, which I think is the bug, is that I still can't use a computed method for this. However, I can use a ref and watchEffect to achieve the same thing.

App.vue

<script setup>
  import {watchEffect, computed, ref, toRaw} from "vue";
  const props = defineProps({
    options: {
      type: Array,
      default: [{id: 1, display: 'one'}, {id: 2, display: 'two'}]
    }
  })
  import {MyCollection} from "./Collection.js";
  
  const collection1 = ref(null);
  const collection2 = computed(() => new MyCollection(props.options))
  
  // Workaround for not being able to use computed
  watchEffect(() => {
    collection1.value = new MyCollection(props.options)
  })
  watchEffect(() => {
    console.log("collection1", collection1.value.length);
  });
  // THIS WILL NOT FIRE WHEN ADD FILTER IS CLICKED
  watchEffect(() => {
    console.log("collection2", collection2.value.length);
  });
  
  const onClickUpdate1 =() =>  {
    collection1.value.addFilters();
    collection2.value.addFilters();
  }
</script>

<template>
  <div>
    <button @click='onClickUpdate1'>
      Add Filter
    </button>
  </div>
  <div style="display: flex">
    <div style="margin-right: 1rem;">
      Collection 1
      <div v-for="item in collection1" :key="item.id">
        {{item.display}}
      </div>
    </div>
    <div>
      Collection 2
      <div v-for="item in collection2" :key="item.id">
        {{item.display}}
      </div>
    </div>
  </div>
</template>
  • Related