Vue 3 how to watch Map - javascript

I’m passing a map to a component and then emit back new values and update the map. This triggers the watch on the map, but the old and new values passed are the same.
Is it possible in this case to have a watch that receives old values?
(function() {
const ui = {
setup() {
const map = reactive(new Map([
['width', 800],
['height', 400]
]));
function onCompUpdate(evt) {
console.log(map.get('width'), map.get('height'), evt[0], evt[1]);
map.set('width', Number(evt[0]));
map.set('height', Number(evt[1]));
}
watch(map, (n,o)=>{
console.log(n.get('width'), n.get('height'),
o.get('width'), o.get('height'));
});
return { map, onCompUpdate };
},
};
const vueApp = createApp(ui);
vueApp.component('base-input', {
props: ['some' ], emits: ['comp-update'],
setup(props, { emit }) {
let chars = ref(Array.from(props.some.values()).join(','));
function checkInput(val) {
emit('comp-update', val.split(','));
chars.value = val;
}
return { chars, checkInput };
},
template: `<input type="text" spellcheck="false" :value="chars"
input="checkInput($event.target.value)"/>`
});
vueApp.mount('#vue');
})();
HTML:
<div id="vue">
<base-input :some="map" #comp-update="onCompUpdate($event)"></base-input>
</div>
Fiddle:
https://jsfiddle.net/tfoller/sm28u7r0/35/

It only triggers the watch if the whole Map is replaced. If you want to watch the mutation (adding/removing items), you need to set deep: true like so:
watch(myMap, () => {
// myMap was mutated
}, { deep: true })
Working SFC Playground example

This happens because the references to old and new are the same. Here's one workaround. Instead of watching the reference directly, watch the spreading of the reference, which does change. It has to be done inside a function expression since the spread is not reactive:
watch(() => [...map], (nArray, oArray)=>{
const n = new Map(nArray);
const o = new Map(oArray);
console.log(n.get('width'), n.get('height'), o.get('width'), o.get('height'));
});
Since the spread converts the map to an array, inside the watch you can convert the values back to maps.
Here is your updated fiddle

Related

How to push to a map value array and use it as state in React?

I have the following state in React:
const [tasksMap, setTasksMap] = useState(new Map());
with the following map creation:
tasks.forEach((task) => {
const key = `${week.year}-${week.weekNo}`;
if (!tasksMap.has(key)) {
tasksMap.set(key, []);
}
tasksMap.get(key).push(task);
});
How can I set the map tasksMap as the React state?
setTasksMap(tasksMap.set(key, [])); works but has an empty array as value, which does not update when I run tasksMap.get(key).push(task);.
Please try this. Here I used separate variable to store data and set that variable into the state at last.
let mapObj = new Map();
tasks.forEach((task) => {
const key = `${week.year}-${week.weekNo}`;
if (!mapObj.has(key)) {
mapObj.set(key, []);
}
mapObj.get(key).push(task);
});
setTasksMap(mapObj);

Unwanted state changes in a class component with React

Long story short, I have a class component that constructs a poll. Before sending the data to the server I need to transform it a little so it fits the API request. I created a transformData method on my class component that transforms the data derived from the state. As a side effect it sets the data in separate this.state.data property so I can attach it with the API request. The problem is that the method mutates the other properties of the state.
transformData = () => {
const { title, sections } = this.state
const transformedSections = sections.map(section => {
delete section.isOpen
const transformedQuestions = section.questions.map(question => {
question.label = question.question
question.type = toUpper(question.type)
delete question.question
return question
})
section.questions = {
create: transformedQuestions,
}
return section
})
this.setState({
data: {
title,
sections: { create: transformedSections },
},
})
}
So I get this:
state: {
data: {...} //our transformed data
sections: {...} //transformed as well!!
}
instead of getting this:
state: {
data: {...} //our transformed data
sections: {...} //same before calling the method
I re-wrote the method with different approach — basically replaced all Array.map with Array.forEach and it worked as expected.
transformData = () => {
const { title, sections } = this.state
const transformedSections = []
sections.forEach(section => {
const transformedQuestions = []
section.questions.forEach(question => {
transformedQuestions.push({
label: question.question,
type: toUpper(question.type),
max: question.max,
min: question.min,
instruction: question.instruction,
isRequired: question.isRequired,
placeholder: question.placeholder,
})
})
transformedSections.push({
title: section.title,
questions: { create: transformedQuestions },
})
})
this.setState({
data: {
title,
sections: { create: transformedSections },
},
})
Can anyone explain what's going on here? How can I accidentally mutate a state property without explicitly calling this.setState on the aforementioned property? The thing is that the originally written method mutates the state even if I return the data object without calling this.setState whatsoever. Like so:
//This still mutates the state
return {
data: {
title,
sections: { create: transformedSections },
}
}
//without this!
//this.setState({
// data: {
// title,
// sections: { create: transformedSections },
// },
// })
Thanks!
javascript behave like this way,
its called variable referencing.
it works like pointer variable in C.
if your console those variable such as console.log(var1 == var2) it will show true cuz both references from same memory location
if you want to prevent mutate original variable then you have to create another brand new variable to mutate
like this way :
const { title, sections } = this.state
// create new variable following old one (spreading es6 way)
const tempSections = [...sections]
...
also
sections.forEach(section => {
const transformedQuestions = []
const tempQuestions = [...section.questions]
tempQuestions.forEach(question => {
...
always have to create a brand new variable of object/array/... to prevent auto mutation
for further info here
Issue here is of Shallow Copying :
console.log("---- before map -----" , this.state);
const { title, sections } = this.state
// sections is another object, and via map you are mutating inner objects
// beacuse of the shallow Copying
const transformedSections = sections.map(section => {
// any change on section object will direct mutate state
delete section.isOpen //<--- Here you are mutating state
return section
})
// state is muate already
console.log("---- After map -----" , this.state);
You can run the below code snippet and check both console.log, and check for "isOpen": true
Hope this will clear all your doubts :
const { useState , useEffect } = React;
class App extends React.Component {
state = {
title : "questions" ,
sections : [{
isOpen : true ,
questions : ["que1" , "que2" , "que3"]
}]
}
transfromData = () => {
console.log("---- before map -----" , this.state);
const { title, sections } = this.state
// sections is another object, and via map you are mutating inner objects
// beacuse of the shallow Copying
const transformedSections = sections.map(section => {
// any change on section object will direct mutate state
delete section.isOpen //<--- Here you are mutating state
return section
})
console.log("---- After map -----" , this.state);
}
render() {
return (
<div>
<button onClick={this.transfromData}>transfromData</button>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('react-root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react-root"></div>
You should never update the state without using the setState method. It is asyncronous, and if you don't set it properly you never know what might happen - and that's what you're seeing in the first part of your answer. See the docs
By doing
section.questions = {
create: transformedQuestions,
}
you are improperly altering the state, so you'll see this.state.sections transformed as well, because each element inside this.state.sections has now an attribute questions that contains create with the value transformedQuestions

Merge two states in Vuex store

I have a Vuex store with two states.
notes (notes that have been synced with the server/DB)
localNotes (which has not been synced with the server/DB, when synced with the server/DB they will be moved to 'notes' state)
I am using these states to show the notes in a list with a getter. This getter merges the two objects and return the merge objects
The problem I have now is that if I add a new note to one of the states it will not be shown in the note list because the getter doesn't pick the 'change' up. I think this happens because I return a variable instead of a function.
This is my getter:
const getters = {
notesObject: (state, getters, rootState, rootGetters) => {
let result = {};
let mergedObject = {...state.notes, ...state.localNotes};
Object.keys(mergedObject).forEach(key => {
const item = mergedObject[key];
if (rootGetters['tags/activeKey'] === null) {
result[key] = item
} else if (item.tag_id === rootGetters['tags/activeKey']) {
result[key] = item
}
});
return result;
},
};
Example object:
example: {
5: {
title: 'Testing title',
text: 'text'
},
6: {
title: 'Testing title',
text: 'text'
}
}
I hope someone can help me out to find the best solution for this. I was thinking about using a watcher, but I know these need to be avoided.
A solution was to let the watcher merge the two states into a new state.
Then the getter doesn't have to merge the two objects
Vue's reactivity system doesn't detect adding new properties. Try using Vue.set(object, key, value) when adding new notes.
In your mutation function replace state.localObject[payload.id] = payload; with Vue.set(state.localObject, payload.id, payload); The getter should then work properly.

How to compare previous and updated props Vue?

I am getting an array from props, and i should trigger a function when the length of an array is changed. Due to the docs, i should use the following construction "handler: function (val, oldVal)", but it returns the new length.
props: ['array']
watch: {
array: function(val, oldVal) { // watch it
console.log('Prop changed: ', val, ' | was: ', oldVal) // it always returns the new array
}
}
Maybe i should use some of the lifecycle hooks?
Most of JavaScript's built-in array functionality mutates an array rather than creating a new array. Vue will detect the changes but the underlying behaviour remains the same, the array is modified.
For example, array.push(17) adds a new value to the end of the same array, it does not create a new array.
If you modify an array it will notify the watch function but the 'new' array is just the same object as the original array. So Vue is passing you the old array, you've just modified that array. Vue does not store a copy of the original state of that array.
From the docs:
Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.
If you just care about the length you could do something like this:
computed: {
arrayLength () {
return this.array.length
}
},
watch: {
arrayLength (newLength, oldLength) {
// ...
}
}
or more directly:
watch: {
'array.length' (newLength, oldLength) {
// ...
}
}
If you really need the old array then you'll either have to pass in a copy as the prop value, or take a copy within your component.
I don't see anything wrong with your code sample. Below is a working example.
Are you sure the array you are passing as a prop is changing?
Are you sure you are correctly mutating the array?
Check out Vuejs Docs - Array Caveats
Vue.component('child', {
template: '<div>Array length is {{ array.length }}</div>',
props: {
array: {
type: Array,
default: () => [],
},
},
watch: {
array: function(val, oldVal) {
alert(`changed from ${oldVal.length} to ${val.length}`);
},
},
});
new Vue({
el: '#app',
data() {
return {
arr: [1, 2, 3],
};
},
mounted() {
setTimeout(this.updateArr, 3000);
},
methods: {
updateArr() {
this.arr = [1, 2, 3, 4];
},
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<child :array="arr"></child>
</div>

Vue.js - How to properly watch for nested data

I'm trying to understand how to properly watch for some prop variation.
I have a parent component (.vue files) that receive data from an ajax call, put the data inside an object and use it to render some child component through a v-for directive, below a simplification of my implementation:
<template>
<div>
<player v-for="(item, key, index) in players"
:item="item"
:index="index"
:key="key"">
</player>
</div>
</template>
... then inside <script> tag:
data(){
return {
players: {}
},
created(){
let self = this;
this.$http.get('../serv/config/player.php').then((response) => {
let pls = response.body;
for (let p in pls) {
self.$set(self.players, p, pls[p]);
}
});
}
item objects are like this:
item:{
prop: value,
someOtherProp: {
nestedProp: nestedValue,
myArray: [{type: "a", num: 1},{type: "b" num: 6} ...]
},
}
Now, inside my child "player" component I'm trying to watch for any Item's property variation and I use:
...
watch:{
'item.someOtherProp'(newVal){
//to work with changes in "myArray"
},
'item.prop'(newVal){
//to work with changes in prop
}
}
It works but it seems a bit tricky to me and I was wondering if this is the right way to do it. My goal is to perform some action every time prop changes or myArray gets new elements or some variation inside existing ones. Any suggestion will be appreciated.
You can use a deep watcher for that:
watch: {
item: {
handler(val){
// do stuff
},
deep: true
}
}
This will now detect any changes to the objects in the item array and additions to the array itself (when used with Vue.set). Here's a JSFiddle: http://jsfiddle.net/je2rw3rs/
EDIT
If you don't want to watch for every change on the top level object, and just want a less awkward syntax for watching nested objects directly, you can simply watch a computed instead:
var vm = new Vue({
el: '#app',
computed: {
foo() {
return this.item.foo;
}
},
watch: {
foo() {
console.log('Foo Changed!');
}
},
data: {
item: {
foo: 'foo'
}
}
})
Here's the JSFiddle: http://jsfiddle.net/oa07r5fw/
Another good approach and one that is a bit more elegant is as follows:
watch:{
'item.someOtherProp': function (newVal, oldVal){
//to work with changes in someOtherProp
},
'item.prop': function(newVal, oldVal){
//to work with changes in prop
}
}
(I learned this approach from #peerbolte in the comment here)
VueJs deep watch in child objects
new Vue({
el: "#myElement",
data: {
entity: {
properties: []
}
},
watch: {
'entity.properties': {
handler: function (after, before) {
// Changes detected. Do work...
},
deep: true
}
}
});
Personally I prefer this clean implementation:
watch: {
myVariable: {
handler(newVal, oldVal){ // here having access to the new and old value
// do stuff
console.log(newVal, oldVal);
},
deep: true,
/*
Also very important the immediate in case you need it,
the callback will be called immediately after the start
of the observation
*/
immediate: true
}
}
How if you want to watch a property for a while and then to un-watch it?
Or to watch a library child component property?
You can use the "dynamic watcher":
this.$watch(
'object.property', //what you want to watch
(newVal, oldVal) => {
//execute your code here
}
)
The $watch returns an unwatch function which will stop watching if it is called.
var unwatch = vm.$watch('a', cb)
// later, teardown the watcher
unwatch()
Also you can use the deep option:
this.$watch(
'someObject', () => {
//execute your code here
},
{ deep: true }
)
Please make sure to take a look to docs
Another way to add that I used to 'hack' this solution was to do this:
I set up a seperate computed value that would simply return the nested object value.
data : function(){
return {
countries : {
UnitedStates : {
value: "hello world";
}.
},
};
},
computed : {
helperName : function(){
return this.countries.UnitedStates.value;
},
},
watch : {
helperName : function(newVal, oldVal){
// do this...
}
}
Tracking individual changed items in a list
If you want to watch all items in a list and know which item in the list changed, you can set up custom watchers on every item separately, like so:
var vm = new Vue({
data: {
list: [
{name: 'obj1 to watch'},
{name: 'obj2 to watch'},
],
},
methods: {
handleChange (newVal, oldVal) {
// Handle changes here!
// NOTE: For mutated objects, newVal and oldVal will be identical.
console.log(newVal);
},
},
created () {
this.list.forEach((val) => {
this.$watch(() => val, this.handleChange, {deep: true});
});
},
});
If your list isn't populated straight away (like in the original question), you can move the logic out of created to wherever needed, e.g. inside the .then() block.
Watching a changing list
If your list itself updates to have new or removed items, I've developed a useful pattern that "shallow" watches the list itself, and dynamically watches/unwatches items as the list changes:
// NOTE: This example uses Lodash (_.differenceBy and _.pull) to compare lists
// and remove list items. The same result could be achieved with lots of
// list.indexOf(...) if you need to avoid external libraries.
var vm = new Vue({
data: {
list: [
{name: 'obj1 to watch'},
{name: 'obj2 to watch'},
],
watchTracker: [],
},
methods: {
handleChange (newVal, oldVal) {
// Handle changes here!
console.log(newVal);
},
updateWatchers () {
// Helper function for comparing list items to the "watchTracker".
const getItem = (val) => val.item || val;
// Items that aren't already watched: watch and add to watched list.
_.differenceBy(this.list, this.watchTracker, getItem).forEach((item) => {
const unwatch = this.$watch(() => item, this.handleChange, {deep: true});
this.watchTracker.push({ item: item, unwatch: unwatch });
// Uncomment below if adding a new item to the list should count as a "change".
// this.handleChange(item);
});
// Items that no longer exist: unwatch and remove from the watched list.
_.differenceBy(this.watchTracker, this.list, getItem).forEach((watchObj) => {
watchObj.unwatch();
_.pull(this.watchTracker, watchObj);
// Optionally add any further cleanup in here for when items are removed.
});
},
},
watch: {
list () {
return this.updateWatchers();
},
},
created () {
return this.updateWatchers();
},
});
I've found it works this way too:
watch: {
"details.position"(newValue, oldValue) {
console.log("changes here")
}
},
data() {
return {
details: {
position: ""
}
}
}
Not seeing it mentioned here, but also possible to use the vue-property-decorator pattern if you are extending your Vue class.
import { Watch, Vue } from 'vue-property-decorator';
export default class SomeClass extends Vue {
...
#Watch('item.someOtherProp')
someOtherPropChange(newVal, oldVal) {
// do something
}
...
}
My problem with the accepted answer of using deep: true, is that when deep-watching an array, I can't easily identify which element of the array contains the change. The only clear solution I've found is this answer, which explains how to make a component so you can watch each array element individually.
None of the answer for me was working. Actually if you want to watch on nested data with Components being called multiple times. So they are called with different props to identify them.
For example <MyComponent chart="chart1"/> <MyComponent chart="chart2"/>
My workaround is to create an addionnal vuex state variable, that I manually update to point to the property that was last updated.
Here is a Vuex.ts implementation example:
export default new Vuex.Store({
state: {
hovEpacTduList: {}, // a json of arrays to be shared by different components,
// for example hovEpacTduList["chart1"]=[2,6,9]
hovEpacTduListChangeForChart: "chart1" // to watch for latest update,
// here to access "chart1" update
},
mutations: {
setHovEpacTduList: (state, payload) => {
state.hovEpacTduListChangeForChart = payload.chart // we will watch hovEpacTduListChangeForChart
state.hovEpacTduList[payload.chart] = payload.list // instead of hovEpacTduList, which vuex cannot watch
},
}
On any Component function to update the store:
const payload = {chart:"chart1", list: [4,6,3]}
this.$store.commit('setHovEpacTduList', payload);
Now on any Component to get the update:
computed: {
hovEpacTduListChangeForChart() {
return this.$store.state.hovEpacTduListChangeForChart;
}
},
watch: {
hovEpacTduListChangeForChart(chart) {
if (chart === this.chart) // the component was created with chart as a prop <MyComponent chart="chart1"/>
console.log("Update! for", chart, this.$store.state.hovEpacTduList[chart]);
},
},
I used deep:true, but found the old and new value in the watched function was the same always. As an alternative to previous solutions I tried this, which will check any change in the whole object by transforming it to a string:
created() {
this.$watch(
() => JSON.stringify(this.object),
(newValue, oldValue) => {
//do your stuff
}
);
},
For anyone looking for Vue 3
import { watch } from 'vue';
...
...
watch(
() => yourNestedObject, // first param, your object
(currValue, prevValue) => { // second param, watcher callback
console.log(currValue, prevValue);
},
{ deep: true } // third param, for deep checking
);
You can refer to the documentation here: https://v3.vuejs.org/guide/reactivity-computed-watchers.html#watch
Here's a way to write watchers for nested properties:
new Vue({
...allYourOtherStuff,
watch: {
['foo.bar'](newValue, oldValue) {
// Do stuff here
}
}
});
You can even use this syntax for asynchronous watchers:
new Vue({
...allYourOtherStuff,
watch: {
async ['foo.bar'](newValue, oldValue) {
// Do stuff here
}
}
});
https://vuejs.org/guide/essentials/watchers.html#deep-watchers
export default {
watch: {
someObject: {
handler(newValue, oldValue) {
// Note: `newValue` will be equal to `oldValue` here
// on nested mutations as long as the object itself
// hasn't been replaced.
},
deep: true
}
}
}

Categories