Why does Pinia state mutate when using getters? - vue.js

I would expect that when calling a getter from the store and assigning it to a variable, any changes to that variable would not result in the store state being updated, as I'm not calling the state directly.
This is true for primitive data, but not for object references.
<script setup>
import { ref } from 'vue'
import { useStudyStore } from '#/study/studyStore'
const StudyStore = useStudyStore()
let string = ref(StudyStore.getString)
string.value = 'bar2' // Does NOT update store
let object = ref(StudyStore.getObject)
object.value.a = 2 // DOES update store
const simpleArray = ref(StudyStore.getSimpleArray)
simpleArray.value[0] = 5 // DOES update store
</script>
// Store
export const useStudyStore = defineStore('study', {
state: () => ({
string: 'bar',
object: { a: 1 },
simpleArray: [1, 2, 3, 4],
}),
getters: {
getString(state) {
return state.string
},
getObject(state) {
return state.object
},
getSimpleArray(state) {
return state.simpleArray
},
},
})
I understand that this is due to how referencing works in Javascript, but I expected Pinia to handle this in some way to prevent accidental updating of the store. Is it really the case that any time we want to protect a non-primitive state value in the store from being mutated we need to parse/stringify them?
const copy = JSON.parse(JSON.stringify(StudyStore.getSimpleArray))

This is caused by the simpleArray being deeply reactive
Generally, you'd expect that everything in store is deeply reactive
If you want to explicitly remove reactivity, use
// the object will be non-reactive, you'll have to `store.simpleArray = store.simpleArray` to update it
simpleArray: markRaw([1, 2, 3, 4])
// the returned value would be non-reactive, but would stealthy change reactive object
getSimpleArray() {
return toRaw(this.simpleArray)
}
// will disallow changes on TS level
getSimpleArray() {
return this.simpleArray as DeepReadonly<typeof this.this.simpleArray>
}
// will disallow changes on proxy level
getSimpleArray() {
return deepReadonly(this.simpleArray)
}
(readonlys may be not is #vue but in #vueuse/core or whatever)
why deeply reactive, say you have deep options
options: Record<someId, { colorOfSomeId: string }>
obviously expected to be deep reactive

Related

Vue3 Composition API with Typescript Default Values for nested objects not working as expected

I want to be able to set default values for nested datasets in Vue, basically, if a key does not have a value in the dataset that is being parsed to the component, I would like that value to be filled in automatically. This kinda works looking into this example here. However, my sample which is nested with several keys in the main object seems not to work. Why is that? what am I doing wrong?
Test Component
<script setup lang="ts">
import {defineProps, PropType} from 'vue';
export type KitchenType = {
windows: number;
};
export type DefaultTestType = {
kitchen?: KitchenType;
rooms?: number;
};
const props = defineProps({
dataset: {
type: Object as PropType<DefaultTestType>,
default: () => ({
kitchen: {
windows: 5,
},
rooms: 3,
})
},
});
console.log('IAM DATASET: ', props.dataset);
</script>
Calling the Component
<DefaultTest :dataset="{
rooms: 5,
}" />
Console.log result
{ rooms: 5 }
Console.log expected result
{ kitchen: { windows: 5, }, rooms: 5 }
I'm using Laravel 9 with Vite, Inertia and Vue3 and have enabled the reactivityTransform: true in the vite.config file as told here through here
If I don't pass any dataset object, I am getting all the default values
I do not think it is possible to do such a thing with the use of default. Although you can create a default object then with the use of the Object.assign() method create a new object with default values, which will be overwritten by the prop you will receive.
const deafult = { kitchen: { windows: 5, }, rooms: 5 };
const recive = {rooms: 3}; //a prop
const mergedValues = Object.assign(deafult, recive);
console.log(mergedValues)
Have you ever try withDefaults macro for define props with default value

Update all object property of an array using vuex

I am trying to update a single property of an object from an array using vuex.
here is my code in store file.
export default{
namespaced: true,
state: {
customers: null,
},
mutations: {
UPDATE_MODIFIED_STATE(state, value) {
state.customers = [
...state.customers.filter(item => item.Id !== value.Id),
value,
];
},
},
And below code is from my .vue file.
export default {
computed: {
customerArray() {
return this.$store.state.CustomerStore.customers;
},
},
methods: {
...mapMutations('CustomerStore', ['UPDATE_MODIFIED_STATE']),
updateCustomers() {
if(someCondition) {
this.customerArray.forEach((element) => {
element.IsModified = true;
this.UPDATE_MODIFIED_STATE(element);
});
}
/// Some other code here
},
},
};
As you can see I want to update IsModified property of object.
It is working perfectly fine. it is updating the each customer object.
Just want to make sure, is it correct way to update array object or I should use Vue.set.
If yes, I should use Vue.set, then How can I use it here.
You are actually not mutating your array, what you do is replacing the original array with a new array generated by the filter function and the passed value. So in your example there is no need to use Vue.set.
You can find more information about replacing an array in the vue documentation.
The caveats begin however when you directly set an item with the index or when you modify the length of the array. When doing this the data will no longer be reactive, you can read more about this here.
For example, consider the following inside a mutation:
// If you update an array item like this it will no longer be reactive.
state.customers[0] = { Id: 0, IsModified: true }
// If you update an array item like this it will remain reactive.
Vue.set(state.customers, 0, { Id: 0, IsModified: true })

Vue.js 2: action upon state variable change

I am using a simple state manager (NOT vuex) as detailed in the official docs. Simplified, it looks like this:
export const stateholder = {
state: {
teams: [{id: 1, name:'Dallas Cowboys'}, {id: 2, name:'Chicago Bears'}, {id: 3, name:'Philadelphia Eagles'}, {id:4, name:'L.A. Rams'}],
selectedTeam: 2,
players: []
}
getPlayerList: async function() {
await axios.get(`http://www.someapi.com/api/teams/${selectedTeam}/players`)
.then((response) => {
this.state.players = response.data;
})
}
}
How can I (reactively, not via the onChange event of an HTML element) ensure players gets updated (via getPlayerList) every time the selectedTeam changes?
Any examples of simple state that goes a little further than the official docs? Thank you.
Internally, Vue uses Object.defineProperty to convert properties to getter/setter pairs to make them reactive. This is mentioned in the docs at https://v2.vuejs.org/v2/guide/reactivity.html#How-Changes-Are-Tracked:
When you pass a plain JavaScript object to a Vue instance as its data
option, Vue will walk through all of its properties and convert them
to getter/setters using Object.defineProperty.
You can see how this is set up in the Vue source code here: https://github.com/vuejs/vue/blob/79cabadeace0e01fb63aa9f220f41193c0ca93af/src/core/observer/index.js#L134.
You could do the same to trigger getPlayerList when selectedTeam changes:
function defineReactive(obj, key) {
let val = obj[key]
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
return val;
},
set: function reactiveSetter(newVal) {
val = newVal;
stateholder.getPlayerList();
}
})
}
defineReactive(stateholder.state, 'selectedTeam');
Or you could set it up implicitly using an internal property:
const stateholder = {
state: {
teams: [/* ... */],
_selectedTeam: 2,
get selectedTeam() {
return this._selectedTeam;
},
set selectedTeam(val) {
this._selectedTeam = val;
stateholder.getPlayerList();
},
players: []
},
getPlayerList: async function() {
/* ... */
},
};
Your question is also similar to Call a function when a property gets set on an object, and you may find some more information there.
You could use v-on:change or #change for short to trigger getPlayerList.
Here a fiddle, simulating the request with setTimeout.

Can't get data of computed state from store - Vue

I'm learning Vue and have been struggling to get the data from a computed property. I am retrieving comments from the store and them processing through a function called chunkify() however I'm getting the following error.
Despite the comments being computed correctly.
What am I doing wrong here? Any help would be greatly appreciated.
Home.vue
export default {
name: 'Home',
computed: {
comments() {
return this.$store.state.comments
},
},
methods: {
init() {
const comments = this.chunkify(this.comments, 3);
comments[0] = this.chunkify(comments[0], 3);
comments[1] = this.chunkify(comments[1], 3);
comments[2] = this.chunkify(comments[2], 3);
console.log(comments)
},
chunkify(a, n) {
if (n < 2)
return [a];
const len = a.length;
const out = [];
let i = 0;
let size;
if (len % n === 0) {
size = Math.floor(len / n);
while (i < len) {
out.push(a.slice(i, i += size));
}
} else {
while (i < len) {
size = Math.ceil((len - i) / n--);
out.push(a.slice(i, i += size));
}
}
return out;
},
},
mounted() {
this.init()
}
}
Like I wrote in the comments, the OPs problem is that he's accessing a store property that is not available (probably waiting on an AJAX request to come in) when the component is mounted.
Instead of eagerly assuming the data is present when the component is mounted, I suggested that the store property be watched and this.init() called when the propery is loaded.
However, I think this may not be the right approach, since the watch method will be called every time the property changes, which is not semantic for the case of doing prep work on data. I can suggest two solutions that I think are more elegant.
1. Trigger an event when the data is loaded
It's easy to set up a global messaging bus in Vue (see, for example, this post).
Assuming that the property is being loaded in a Vuex action,the flow would be similar to:
{
...
actions: {
async comments() {
try {
await loadComments()
EventBus.trigger("comments:load:success")
} catch (e) {
EventBus.trigger("comments:load:error", e)
}
}
}
...
}
You can gripe a bit about reactivity and events going agains the reactive philosophy. But this may be an example of a case where events are just more semantic.
2. The reactive approach
I try to keep computation outside of my views. Instead of defining chunkify inside your component, you can instead tie that in to your store.
So, say that I have a JavaScrip module called store that exports the Vuex store. I would define chunkify as a named function in that module
function chunkify (a, n) {
...
}
(This can be defined at the bottom of the JS module, for readability, thanks to function hoisting.)
Then, in your store definition,
const store = new Vuex.Store({
state: { ... },
...
getters: {
chunkedComments (state) {
return function (chunks) {
if (state.comments)
return chunkify(state.comments, chunks);
return state.comments
}
}
}
...
})
In your component, the computed prop would now be
computed: {
comments() {
return this.$store.getters.chunkedComments(3);
},
}
Then the update cascase will flow from the getter, which will update when comments are retrieved, which will update the component's computed prop, which will update the ui.
Use getters, merge chuckify and init function inside the getter.And for computed comment function will return this.$store.getters.YOURFUNC (merge of chuckify and init function). do not add anything inside mounted.

How to update an object in 'state' with react redux?

In my reducer, suppose originally I have this state:
{
"loading": false,
"data": {
"-L1LwSwW97KkwdSnYvsc": {
"likeCount": 10,
"liked": false, // I want to update this property
"commentCount": 5
},
"-L1EY2_fqzn7sM1Mbf_F": {
"likeCount": 8,
"liked": true,
"commentCount": 22
}
}
}
Now, I want to update liked property inside -L1LwSwW97KkwdSnYvsc object, which is inside data object and make it true. This is what I've been trying, but apparently, it's wrong, because after I've updated the state, the componentWillReceiveProps function inside a component that listens to the state change does not get triggered:
var { data } = state;
data['-L1LwSwW97KkwdSnYvsc'].liked = !data['-L1LwSwW97KkwdSnYvsc'].liked;
return { ...state, data };
Could you please specify why it's wrong and how I should change it to make it work?
You're mutating state! When you destructure:
var { data } = state;
It's the same as:
var data = state.data;
So when you do:
data[…].liked = !data[…].liked
You're still modifying state.data which is in turn mutating state. That's never good - use some nested spread syntax:
return {
...state,
data: {
...state.data,
'-L1LwSwW97KkwdSnYvsc': {
...state.data['-L1LwSwW97KkwdSnYvsc'],
liked: !state.data['-L1LwSwW97KkwdSnYvsc'].liked
}
}
};
Using spread operator is good until you start working with deeply nested state and/or arrays(remember spread operator does a shallow copy only).
I would rather recommend you starting working with immutability-helper instead. It is a React recommendation and it will let your code more readable and bug free.
Example:
import update from "immutability-helper";
(...)
const toggleLike = !state.data["-L1LwSwW97KkwdSnYvsc"].liked
return update(state, {
data: {
"-L1LwSwW97KkwdSnYvsc": {
like: {
$set: toggleLike
}
}
}
})