How do I clear a reactive array in Vue3 using composition? - vue.js

I have the following in my project...
const basicRecords = reactive([]);
It basically just houses an array of objects. I want to be able to refresh this array, that would require me to remove all of the records and add them back. To do this I have done the following...
basicRecords.splice(0);
basicRecords.push(...recordResult.data);
This seems to work but also seems super hacky.
What is the proper way to reset a reactive array in Vue3?

Try to use Object.assign to keep it reactive :
const basicRecords = reactive([]);
Object.assign(basicRecords, recordResult.data)
or define basicRecords as inner field of reactive state :
const state = reactive({basicRecords : []});
state.basicRecords = recordResult.data
or with ref :
const basicRecords = ref([]);
basicRecords.value = recordResult.data

Related

Vue computed() not triggered on reactive map

I have a reactive around a map that's initially empty: const map = reactive({});, and a computed that tells if the map has a key "key": const mapContainsKeyComputed = computed(() => map.hasOwnProperty("key")). The computed doesn't get updated when I change the map.
I stuck with this issue for a day and managed to come up with a minimum example that demonstrates the issue:
<script setup>
import {computed, reactive, ref, watch} from "vue";
const map = reactive({});
const key = "key";
const mapContainsKeyComputed = computed(() => map.hasOwnProperty(key))
const mapContainsKeyWatched = ref(map.hasOwnProperty(key));
watch(map, () => mapContainsKeyWatched.value = map.hasOwnProperty(key))
</script>
<template>
Map: {{map}}
<br/>
Computed: does map contain "key"? {{mapContainsKeyComputed}}
<br/>
Watch: does map contain key? {{mapContainsKeyWatched}}
<br/>
<button #click="map[key] = 'value'">add key-value</button>
</template>
I've read a bunch of stackoverflow answers and the Vue docs, but I still can't figure it out.
why mapContainsKeyComputed doesn't get updated?
if the reactive doesn't "track" adding or removing keys to the map, why the Map: {{map}} (line 14) updates perfectly fine?
when I replace the map{} with an array[] and "hasOwnProperty" with "includes()", it works fine. How's that different?
how do I overcome this issue without the ugly "watch" solution where the "map.hasOwnProperty(key)" has to be duplicated?
EDIT: as mentioned by #estus-flask, this was a VueJS bug fixed in 3.2.46.
Vue reactivity needs to explicitly support reactive object methods. hasOwnProperty is rather low-level so it hasn't been supported for some time. Without the support, map.hasOwnProperty(key) tries to access key on non-reactive raw object and doesn't trigger the reactivity, so the first computed call doesn't set a listener that could be triggered with the next map change.
One way this could be fixed is to either define key initially (as suggested in another answer), this is the legacy way to make reactivity work in both Vue 2 and 3:
const map = reactive({ key: undefined })
Another way is to access missing key property on reactive object:
const mapContainsKeyComputed = computed(() => map[key] !== undefined)
Yet another way is to use in operator. Since Vue 3 uses Proxy for reactivity, that a property is accessed can be detected by has trap:
const mapContainsKeyComputed = computed(() => key in map)
The support for hasOwnProperty has been recently added in 3.2.46, so the code from the question is supposed to be workable with the latest Vue version.
map is not really a map. This would be different in any Vue 3 version if Map were used, it's supported by Vue and it's expected that map.has(key) would trigger reactivity.

Should i use let or const in vue 3 ref variables?

I am creating a new Vue 3 project and i saw many people online declaring refs like this.
const myVariable = ref(false)
Why are we using const all of a sudden in Vue 3? I get that refs wrap them in some way to make them editable but i still don't get why not declaring them like this:
let myVariable = ref(false)
I know it may sound a foolish question for Vue 3 devs but i cannot understand the reason behind changing values to constants.
During the meanwhile i am using the const declaration in composition API but i'd like to acknowledgwe the reason behind
it's preferences but the argument why const is used is when the value is not changing e.g:
const name = 'John';
// Shouldn't work.
name = 'Bill';
With ref(), you don't replace the variable but the property
const name = ref('John');
name.current = 'Bill';
Here's how eslint explains it:
If a variable is never reassigned, using the const declaration is better.
const declaration tells readers, “this variable is never reassigned,” reducing cognitive load and improving maintainability.
Docs (at the time of writing): https://eslint.org/docs/latest/rules/prefer-const

Is ref() & watch() combination equivalent to computed() in Vue 3?

I have a reactive object
team
and it has a property
players
I want to also make players reactive (ease of use by exporting it from a custom hook).
Are these two pieces of code equivalent ?
(I suppose they are not, but I want to know the gotchas of using one over the other way)
const players = computed(() => team.value.players);
const players = ref(team.value.players);
watch(team, () => players.value = team.value.players);
Which one should I use ? Is there a big difference (optimization ?)
What about usage with v-model ?
It's recommended to use computed property with getter/setter in order to be able to bind it to v-model :
const players = computed({
get() => team.value.players,
set(newVal){
}
});

Vue.set() and push() for Vuex store

I have a mutator that attempts to make the follow update:
state.forms[1].data.metrics.push(newArrayItem)
forms is an object with 1 as a key
metrics is an array
For some reason, Vuex successfully updates, but components don't react to this change.
I was reading about Vue.set() on https://v2.vuejs.org/v2/guide/list.html#Object-Change-Detection-Caveats
But I'm not sure how to apply it, or even if it's the right answer at all here.
Thanks for the help.
this.$forceUpdate is working, which is a bit strange because the component loading the data uses computed properties.
Form state is setup initially like so:
const state = {
forms: {}
};
New forms are pushed like so:
state.forms[id] = { formName: payload.formName, data: {metrics: []}};
And new metrics are added like so:
var result = getParent(state, payload.path);
result.metricParent.data.metrics.push({ id: newMetricId(state, result.formId), ...payload.metric });
Your problem is not with pushing to arrays. Your problem is in this line:
state.forms[id] = { formName: payload.formName, data: {metrics: []}};
Because you are creating a new property of state.forms without using Vue.set(), the form is not reactive, so when you push to the metrics array later, it's not recognized.
Instead, you should do this:
Vue.set(state.forms, id, { formName: payload.formName, data: {metrics: []}});
Then, pushing to state.forms[id].data.metrics should work as expected.
Vue setup reactive data looking for how the state/data is setup, by example if in a regular component you define the data like {x: {y: 10}} and you change the data somehow this.x.y = 20; it’s going to work, because Vue make the object with that structure reactive (because is the setup structure) based on that if you try to do, this.x.z = 10; not works because “z” not exists, and you need to tell to Vue that you need to make it reactive, this is when this.$set(this.x, “z”, 10); enters, it’s basically saying “make this data reference in position ‘z’ reactive”, after this point direct calls to this.x.z = ? works, in vuex the same happens, use Vue.set(state.forms, 1, { formName: payload.formName, data: {metrics: []}}); after that the reference to state.forms[1] (including sub data) is now reactive!

Dynamically creating a reactive array in the Vuex's state

My component would like to add a new reactive-array field to the SST (vuex). I tried in beforeCreate hook, but the added array is not reactive; it's just a plain JS array.
Note that this is not the same as adding/removing elements from an existing array created at the Vue's initialization time. Such arrays are "wrapped" and become reactive as expected, mindful of "Array Change Detection" gotchas.
In my case, I'm trying to dynamically add an entirely new field of array type to the SST and make it reactive at the same time. Possible?
Have a look at Reactivity in Depth - Change Detection Caveats:
Change Detection Caveats
Due to the limitations of modern JavaScript, Vue cannot detect property
addition or deletion.
Since Vue performs the getter/setter conversion process during
instance initialization, a property must be present in the data object
in order for Vue to convert it and make it reactive.
But you say you are adding an array dynamically:
I'm trying to dynamically add an entirely new field of array type to the SST and make it reactive at the same time. Possible?
From the docs (bold is mine):
Vue does not allow dynamically adding new root-level reactive properties to an already created instance. However, it’s possible to add reactive properties to a nested object using the Vue.set(object, key, value) method:
Vue.set(vm.someObject, 'myArrayName', [1,2,3]);
Which should help you making your array reactive.
I see two problems here:
add dynamically array using vuex.
add dynamically element to this array and render this element.
I've initiate array if not exist in add method because when I'm receiving data from server myArray is not exist.
My solutuion below:
myVuexArray.js
import Vue from 'vue'
const state = {
myObject: {
myArray: [],
}
}
const getters = {
getMyArray: state => {
return state.myObject.myArray;
}
}
const mutations = {
addElementToArray(state, value) {
if (state.myObject.myArray === null || state.myObject.myArray === undefined || state.myObject.myArray === '') {
// initiate array
state.myObject.myArray = [];
}
// add new element to array
Vue.set(
state.myObject.myArray,
state.myObject.myArray.length,
value
);
// creates a new array everytime this solves the reactivity issue
Vue.set(state, 'myObject.myArray', state.myObject.myArray);
return state.myObject.myArray;
},
removeElementFromArray(state, index) {
state.myObject.myArray.splice(index, 1);
}
}
export default {
state,
mutations,
getters
}
Best regards
Dynamic module registration could help you to achieve this :
https://vuex.vuejs.org/en/modules.html
This would allow you to dynamically register a new module containing your array field in the beforeCreate hook.