Mutation payload changes value by itself in vuex store mutation - javascript

I'm trying to build an Electron app with VueJS using the electron-vue boilerplate. I have a mutation which updates parts of the state based on the payload it receives.
However, somewhere between the action call and the mutation, the property payload.myItem.id changes without any intention.
The action is called by a Vue modal component:
handleModalSave() {
let payload = {
layerId: this.layer.id,
myItem: {
id: this.editLayerForm.id,
text: this.editLayerForm.text
}
}
console.log('save', payload.myItem.id)
this.$store.dispatch('addLayerItem', payload)
}
Here are said action and mutation:
// Action
addLayerItem: ({ commit }, payload) => {
commit('ADD_LAYER_ITEM', payload)
}
// Mutation
ADD_LAYER_ITEM: (state, payload) => {
console.log('mutation', payload.myItem.id)
let layer = state.map.layers.find(layer => layer.id === payload.layerId)
if (payload.myItem.id !== '') {
// Existing item
let itemIdx = layer.items.findIndex(item => item.id === payload.myItem.id)
Vue.set(layer.items, itemIdx, payload.myItem)
} else {
// New item
payload.myItem.id = state.cnt
layer.items.push(payload.myItem)
}
}
Here is a screenshot of the console logs:
As far as I can see, there is no command to change myItem.id between console.log('save', payload) and console.log('mutation', payload). I use strict mode, so there is no other function changing the value outside of the mutation.
Edit
I updated the console.logs() to display the property directly instead of the object reference.

As far as I can see, there is no command to change myItem.id between console.log('save', payload) and console.log('mutation', payload).
It doesn't need to change between the console logging.
In the pictures you've posted you'll see a small, blue i icon next to the console logging. If you hover over that you'll get an explanation that the values shown have just been evaluated.
When you log an object to the console it grabs a reference to that object. It doesn't take a copy. There are several reasons why taking a copy is not practical so it doesn't even try.
When you expand that object by clicking in the console it grabs the contents of its properties at the moment you click. This may well differ from the values they had when the object was logged.
If you want to know the value at the moment it was logged then you could use JSON.stringify to convert the object into a string. However that assumes it can be converted safely to JSON, which is not universally true.
Another approach is to change the logging to directly target the property you care about. console.log(payload.myItem.id). That will avoid the problem of the logging being live by logging just the string/number, which will be immutable.
The line that changes the id appears to be:
payload.myItem.id = state.cnt
As already discussed, it is irrelevant that this occurs after the logging. The console hasn't yet grabbed the value of the property. It only has a reference to the payload object.
The only mystery for me is that the two objects you've logged to the console aren't both updated to reflect the new id. In the code they appear to be the same object so I would expect them to be identical by the time you expand them. Further, one of the objects shows evidence of reactive getters and setters whereas the other does not. I could speculate about why that might be but most likely it is caused by code that hasn't been provided.
I use strict mode, so there is no other function changing the value outside of the mutation.
Strict mode only applies to changing properties within store state. The object being considered here is not being held in store state until after the mutation. So if something were to change the id before the mutation runs it wouldn't trigger a warning about strict mode.

Okay I found the root cause for my issue. Vuex was configured to use the createPersistedState plugin which stores data in local storage. Somehow, there was an issue with that and data got mixed up between actual store and local storage. Adding a simple window.localStorage.clear() in main.js solved the problem!

Related

State Mutation detected when updating Redux Values

So I'm fairly new to React-Redux and I'm facing this problem where if i update this array with a new object in redux, i'm getting the following error
Uncaught Invariant Violation: A state mutation was detected between dispatches, in the path `settingsObject.arrayForSettings.0.updatedObject`. This may cause incorrect behavior.
The Problem only arises when i update the state in redux with new values, On previous values there is no error. The piece of code that is giving me the error is as follows.
let tempArrayForSettings = [...props.arrayForSettings];
tempArrayForSettings.forEach((element:any) => {
if(element.settingsType === "DesiredSettingType")
{
//modify elements of a JSON object and add it to the element
element.updatedObject = JSONUpdatedObject;
}
//call the action method to update the redux state
updateAction(tempArrayForSettings);
});
The error is pointing me to the following link : Redux Error
I know I'm not updating the object I'm getting from props instead I'm making a copy using the spread operator so I really don't know why the error is coming whenever I'm firing the updateAction function.
Well, your line element.updatedObject = JSONUpdatedObject; is modifying the object in the Redux store. element is a direct reference to your store object. You would need to do an immutable copy here - your spread above only does a shallow copy, but the items here are still the same.
Generally, you should do logic like this not in your component, but within your Reducer. (see the Style Guide on this topic) That also gives you the benefit that you don't need to care about immutability, as within createSlice reducers you can simply modify data.
You're updating the object inside the array, so the spread operator that you've created above only clones the array itself, not the objects inside the array. In Javascript, objects are passed by reference (read more here)
To fix this warning, you'd need to copy the object itself, and to do that, you'd need to find the specific object in the array that you'd like to change.
Try this:
const { arrayForSettings } = props;
const modifiedSettings = arrayForSettings.map((element:any) => {
if(element.settingsType === "DesiredSettingType")
{
//modify elements of a JSON object and add it to the element
return {
...element,
updatedObject: JSONUpdatedObject,
}
}
return element;
}
updateAction(modifiedSettings);
});
Also, it's recommended that this logic lives on the reducer side, not in your component.

React: Missing property in state object

So I started learning React and after doing some basics (counters, todo lists) I decided to do something more challenging; a chessboard with pieces that can be moved around by clicking. My main component is Chessboard. It then renders Tiles that may contain a Piece. Information about the game is stored in Chessboard's state, looking like this:
interface State {
board: Record<string, pieceInterface>;
isPieceMoving: boolean;
movingPieceId: string;
}
I handle moving pieces in Chessboard's method onTileClick(id: string). It is passed to a Tile as a prop and when a tile is clicked it's called with a tile id (like "a4", "f6", "h3"). It has following logic. Game can be in two states: I can be currently moving a piece or I can currently do nothing and then start moving a piece by clicking on it. When I start moving a piece I store it's ID (or rather an ID of a tile on witch piece stands) in state.movingPieceId. When I try to place it I check if the tile is empty and then change state.board accordingly. Here is my code:
onTileClick = (id: string): void => {
if (!this.state.isPieceMoving) {
if (this.state.board[id]) {
this.setState({
isPieceMoving: true,
movingPieceId: id,
});
}
} else {
if (!this.state.board[id]) {
this.setState((state) => {
let board: Record<string, pieceInterface> = state.board;
const piece: pieceInterface = board[state.movingPieceId];
delete board[state.movingPieceId];
board[id] = piece;
// console.log(board[id]);
// console.lob(board);
const isPieceMoving: boolean = false;
const movingPieceId: string = "";
return { board, isPieceMoving, movingPieceId };
});
}
}
};
The first part works just fine. But in the second there is some bug, that I cannot find. When I uncomment this console logs the output looks like this (I want to move a piece from "a2" to "a3"):
Object { "My piece representation" }
Object {
a1: { "My piece representation" },
a3: undefined,
a7: { "My piece representation" },
...
}
My code properly "takes" clicked piece from "a2" (board has no "a2" property) but it apparently does not place it back. Even when printing board[id] gives correct object! I was moving logging line console.log(board) up to see where bug happens. And even when I put it directly behind else it still was saying that "a3" was undefined.
I spend hours trying to understand what was happening. I tried to emulate this fragment of code in console, but there it always worked! Can anybody explain to me what am I doing wrong? Is this some weird React's setState mechanism that I haven't learned yet? Or is it some basic javascript thing, that I am not aware of? Any help would we wonderful, because I cannot move on and do anything else while being stuck here.
EDIT #1
Ok. So I was able to fix this bug by deep copying state.board into board (inspiredy by #Linda Paiste's comment). I just used simple JSON stringify/parse hack:
let board: Record<string, pieceInterface> = JSON.parse(JSON.stringify(state.board));
This fixes the bug but I have no idea why. I will keep this question open, so maybe someone will explain to me what and why was wrong with my previous code.
EDIT #2
Ok. Thanks to #Linda's explanation and reading the docs i finally understood what I was doing wrong. Linda made detailed explanation in an answer below (the accepted one). Thank you all very much!
Problem: Mutation of State
let board: Record<string, pieceInterface> = state.board;
You are creating a new variable board that refers to the same board object as the one in your actual state. Then you mutate that object:
delete board[state.movingPieceId];
board[id] = piece;
This is an illegal mutation of state because any changes to board will also impact this.state.board.
Solution: Create a New Board
Any array or object that you want to change in your state needs to be a new version of that object.
Your deep copy of board works because this new board is totally independent of your state so you can mutate it without impacting the state. But this is not the best solution. It is inefficient and will cause unnecessary renders because every piece object will be a new version as well.
We just need to copy the objects which are changing: the state and the board.
this.setState((state) => ({
isPieceMoving: false,
movingPieceId: '',
board: {
// copy everything that isn't changing
...state.board,
// remove the moving piece from its current position
[state.movingPieceId]: undefined,
// place the moving piece in its new location
[id]: state.board[state.movingPieceId],
}
}));
Your typescript type for the board property of State should be Partial<Record<string, pieceInterface>> because not every slot on the board has a piece in it.
Very ugly but functional Code Sandbox demo
Edit: About the setState Callback
I heard that using setState with a function makes state a copy of this.state, so I can mutate it.
That is incorrect. You should never mutate state and this is no exception. The advantage of using a callback is that the state argument is guaranteed to be the current value, which is important because setState is asynchronous and there might be multiple pending updates.
Here's what the React docs say (emphasis added):
state is a reference to the component state at the time the change is being applied. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from state and props."
Both state and props received by the updater function are guaranteed to be up-to-date.

Why is this Redux state being mutated?

I am having a problem with prevProps and this.props evaluating as different, despite the structures visually looking the same. I am assuming it is something to do with the arrays, however even after stripping out any changes to the existing state in my reducer, the problem still persists. Within 'Settings' there is an array, but since I am not mutating anything here, I don't see why the comparison would fail in my react component,
case types.UPDATE_USER_SETTINGS_SUCCESS: {
return {
...state,
user: {
...state.user,
settings: {
...state.user.settings,
},
},
};
}
This is the code that I am then using to compare the state in UNSAFE_componentWillReceiveProps.
if (this.props.user.settings !== nextProps.user.settings) {
console.log('is different');
}
The console log is firing each time and I cannot work out why. Dev tools shows the same objects.
(I realise the reducer code won't actually change anything, but I have removed the action payload for now to demonstrate that I am still getting the same problem)
If I understood you correctly you have "is different" in the console log after the UPDATE_USER_SETTINGS_SUCCESS action is fired.
This is to be expected since you are mutating the state. In your reducer you write:
settings: {
...state.user.settings,
}
That means that you do in fact get a new object. This line does not seem to do anything though, so if you remove it, then it should work as expected. Note: you do not need to remove the whole reducer, because even though you do return a new state with some of the fields referencing new objects, if you remove settings spread you will get exactly the same reference to the settings.
P.S. The whole reducer is not doing anything as far as I can see apart from returning a different object for some of the fields. The values are identical, but the references are not so you will get !== to be true.
They always will differ because you make shallow comparison of object references:
// Always true in javascript
{ a: 1 } !== { a: 1 }
That's because you return a new object in the reducer:
case types.UPDATE_USER_SETTINGS_SUCCESS: {
// Thats a new object reference
return { /* state */},
};
}

How to induce reactivity when updating multiple props in an object using VueJS?

I was witnessing some odd behaviour while building my app where a part of the dom wasn't reacting properly to input. The mutations were being registered, the state was changing, but the prop in the DOM wasn't. I noticed that when I went back, edited one new blank line in the html, came back and it was now displaying the new props. But I would have to edit, save, the document then return to also see any new changes to the state.
So the state was being updated, but Vue wasn't reacting to the change. Here's why I think why: https://v2.vuejs.org/v2/guide/reactivity.html#For-Objects
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
Sometimes you may want to assign a number of properties to an existing object, for example using Object.assign() or _.extend(). However, new properties added to the object will not trigger changes. In such cases, create a fresh object with properties from both the original object and the mixin object
The Object in my state is an instance of js-libp2p. Periodically whenever the libp2p instance does something I need to update the object in my state. I was doing this by executing a mutation
syncNode(state, libp2p) {
state.p2pNode = libp2p
}
Where libp2p is the current instance of the object I'm trying to get the DOM to react to by changing state.p2pNode. I can't use $set, that is for single value edits, and I think .assign or .extend will not work either as I am trying to replace the entire object tree.
Why is there this limitation and is there a solution for this particular problem?
The only thing needed to reassign a Vuex state item that way is to have declared it beforehand.
It's irrelevant whether that item is an object or any other variable type, even if overwriting the entire value. This is not the same as the reactivity caveat situations where set is required because Vue can't detect an object property mutation, despite the fact that state is an object. This is unnecessary:
Vue.set(state, 'p2pNode', libp2p);
There must be some other problem if there is a component correctly using p2pNode that is not reacting to the reassignment. Confirm that you declared/initialized it in Vuex initial state:
state: {
p2pNode: null // or whatever initialization value makes the most sense
}
Here is a demo for proof. It's likely that the problem is that you haven't used the Vuex value in some reactive way.
I believe your issue is more complex than the basic rules about assignment of new properties. But the first half of this answer addresses the basics rules.
And to answer why Vue has some restrictions about how to correctly assign new properties to a reactive object, it likely has to do with performance and limitations of the language. Theoretically, Vue could constantly traverse its reactive objects searching for new properties, but performance would be probably be terrible.
For what it's worth, Vue 3's new compiler will supposedly able to handle this more easily. Until then, the docs you linked to supply the correct solution (see example below) for most cases.
var app = new Vue({
el: "#app",
data() {
return {
foo: {
person: {
firstName: "Evan"
}
}
};
},
methods: {
syncData() {
// Does not work
// this.foo.occupation = 'coder';
// Does work (foo is already reactive)
this.foo = {
person: {
firstName: "Evan"
},
occupation: 'Coder'
};
// Also works (better when you need to supply a
// bunch of new props but keep the old props too)
// this.foo = Object.assign({}, this.foo, {
// occupation: 'Coder',
// });
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
Hello {{foo.person.firstName}} {{foo.occupation}}!
<button #click="syncData">Load new data</button>
</div>
Update: Dan's answer was good - probably better than mine for most cases, since it accounts for Vuex. Given that your code is still not working when you use his solution, I suspect that p2pNode is sometimes mutating itself (Vuex expects all mutations in that object to go through an official commit). Given that it appears to have lifecycle hooks (e.g. libp2p.on('peer:connect'), I would not be surprised if this was the case. You may end up tearing your hair out trying to get perfect reactivity on a node that's quietly mutating itself in the background.
If this is the case, and libp2p provides no libp2p.on('update') hook through which you could inform Vuex of changes, then you might want to implement a sort of basic game state loop and simply tell Vue to recalculate everything every so often after a brief sleep. See https://stackoverflow.com/a/40586872/752916 and https://stackoverflow.com/a/39914235/752916. This is a bit of hack (an informed one, at least), but it might make your life a lot easier in the short run until you sort out this thorny bug, and there should be no flicker.
Just a thought, I don't know anything about libp2p but have you try to declare your variable in the data options that change on the update:
data: {
updated: ''
}
and then assigning it a value :
syncNode(state, libp2p) {
this.updated = state
state.p2pNode = libp2p
}

ReactJS: [ this.props ] being impossibly different to [ props in this ]

In a continuous section of code called inside componentDidMount:
console.log('hv props');
for(var i = 0; i<20; i++){
console.log(this);
console.log(this.props);
}
console.log('hv props end');
this is supposed to be having this.props.account.
in all
console.log(this);
this.props.account is an object with fields.
However, in all
console.log(this.props);
this.props.account is null. (the account field of this.props is null)
I have placed this section in different parts of my program, and i have tried adding a time consuming process between console.log(this) and console.log(this.props), but this is still the case.
What could have caused this? Thank you.
console.log won't necessarily show you the structure the object has at the time it is logged, but the structure it has when you clicked the triangle to inspect it. In the meantime (between logging and inspecting in the console) the component might have been rerendered with new props. See Can't iterate over my array/object. Javascript, React Native for basically the same problem.
Here is an example that demonstrates the issue (open your browser console):
var obj = {props: {account: null}};
console.log(obj);
console.log(obj.props);
obj.props = {account: 'not null' };
Note how it shows account: "not null" even though the second log shows account: null:
I bet if you delay the evaluation of console.log(this.props), e.g. via
setTimeout(() => console.log(this.props), 2000)
you would see the value you want.
This was just an explanation of what you are seeing. How you want to solve probably depends on the rest of your application. You certainly can't access this.props.account on mount, but you can do on subsequent renders. Keep in mind that componentDidMount is only called when the component is rendered/mounted the first time, not on subsequent renders. Have a look at the other lifecycle methods.

Categories

Resources