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:
- changed to
script setup
for better readability - changed
computed
toreactive
using the$red
syntax from reactivity transform - You were adding items with
name: 'three'
and displayingitem.display
. I changed that bit to add withdisplay: '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>