React: Missing property in state object - javascript

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.

Related

The correct way to store complex form data in React app

I have a complex form in React containing a dozen sub-components. It also includes my own form components that operate by not single values. Some parts of the form use trees of objects as data.
I have the main component (let's call it EditForm) which stores the whole object tree of data. I assume each component of this form should use a specific part of this main object. I should have the ability to store whole form data into JSON and read it back from JSON (and update all sub-components automatically when JSON changed).
The idea was to refresh only those parts of the form that required when I change the main JSON. But the code which handles this behavior is dramatically big and complex. I have to implement comparison functionality into each sub-component and also transfer parts of the main JSON into props of each sub-component.
const [testobj, setTestobj] = useState({'obj1': {......}, 'obj2': {.......}, .......});
function revalidateSub(sub_name, value)
{
let t2 = cloneDeep(testobj);
t2[sub_name] = value;
setTestobj(t2);
}
return (<MySubComponent subobj={testobj['sub1']} ident={"sub1"} onUpdateData={v => revalidateSub('sub1', v)}/>)
(subobj is the piece of data which is controlled by a specific sub-component, onUpdateData is a handler which is called inside the subcomponent each time when it makes any changes to a controlled piece of data)
This scheme, however, does not work as expected. Because revalidateSub() function stores its own copy of testobj and onUpdateData of each component rewriting the changes of other sub-components...
To be honest, I don't quite understand how to store data correctly for this type of React apps. What do you propose? Maybe I should move the data storage out of React components at all? Or maybe useMemo?
Thank you for the shared experience and examples!
Update1: I have coded the example in the Sandbox, please check
https://codesandbox.io/s/thirsty-browser-xf4u0
To see the problem, please click some times to the "Click Me!" button in object 1, you can see the value of obj1 is changing as well as the value of the main object is changing (which is CORRECT). Then click one time to the "Click Me!" of the obj2. You will see (BOOM!) the value of obj1 is reset to its default state. Which is NOT correct.
I think I have solved this by myself. It turned out, that the setState function may also receive the FUNCTION as a parameter. This function should return a new "state" value.
I have moved revalidateSub() function OUT from the EditForm component, so it does not duplicate the state variable "testobj" anymore. And it is now working as expected.
Codebox Page
First of all, I think the idea is correct. In React components, when the value of the props passed from the state or the parent component changes, re-rendering should be performed. Recognized as a value.
Because of this issue, when we need to update a multilayered object or array, we have to use ... to create a new object or array with an existing value.
It seems to be wrong during the copying process.
I don't have the full code, so a detailed answer is not possible, but I think the bottom part is the problem.
let t2 = cloneDeep(testobj);
There are many other solutions, but trying Immutable.js first will help you find the cause of the problem.
Consider also useReducer.
update
export default function EditForm(props) {
const [testobj, setTestobj] = useState({
sub1: { id: 5, v: 55 },
sub2: { id: 7, v: 777 },
sub3: { id: 9, v: 109 }
});
function revalidateSub(sub_name, value) {
let t2 = cloneDeep(testobj);
t2[sub_name] = value;
return t2;
}
console.log("NEW EditForm instance was created");
function handleTestClick() {
let t2 = cloneDeep(testobj);
t2.sub2.v++;
setTestobj(t2);
}
return (
<div>
<div style={{ textAlign: "left" }}>
<Button onClick={handleTestClick} variant="contained" color="secondary">
Click Me to change from main object
</Button>
<p>
Global value:{" "}
<span style={{ color: "blue" }}>{JSON.stringify(testobj)}</span>
</p>
<MyTestObj
subobj={testobj["sub1"]}
ident={"sub1"}
onUpdateData={(v) => setTestobj((p) => revalidateSub("sub1", v))}
/>
<MyTestObj
subobj={testobj["sub2"]}
ident={"sub2"}
onUpdateData={(v) => setTestobj((p) => revalidateSub("sub2", v))}
/>
<MyTestObj
subobj={testobj["sub3"]}
ident={"sub3"}
onUpdateData={(v) => setTestobj((p) => revalidateSub("sub3", v))}
/>
</div>
</div>
);
}
The reason it bursts when you do the above setState updates the local state asynchronously.
The setState method should be thought of as a single request, not an immediate, synchronous execution. In other words, even if the state is changed through setState, the changed state is not applied immediately after the method is executed.
Thus, your code
setTestobj((p) => revalidateSub(p, "sub3", v))
It is safer to use the first parameter of setstate to get the full state like this!
Thanks to this, I was able to gain new knowledge. Thank you.

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
}

Vue data not available until JSON.stringify() is called

I'm not sure how to tackle this issue because there's quite a bit into it, and the behavior is one I've never seen before from JavaScript or from Vue.js
Of course, I will try to keep the code minimal to the most critical and pieces
I'm using vue-class-component(6.3.2), so my Vue(2.5.17) components look like classes :)
This particular component looks like so:
import GameInterface from '#/GameInterface';
class GameComponent extends Vue {
public gameInterface = GameInterface();
public mounted() {
this.gameInterface.launch();
}
}
GameInterface return an object with a launch method and other game variables.
In the game interface file to method looks something like this:
const GameInterface = function () {
const obj = {
gameState: {
players: {},
},
gameInitialized: false,
launch() => {
game = createMyGame(obj); // set gameInitialized to true
},
};
return obj;
}
export default GameInterface;
Great, it works, the object is passed onto my Phaser game :) and it is also returned by the method, meaning that Vue can now use this object.
At some point I have a getter method in my Vue class that looks like so:
get currentPlayer() {
if (!this.gameInterface.gameInitialized) return null;
if (!this.gameInterface.gameState.players[this.user.id]) {
return null;
}
return this.gameInterface.gameState.players[this.user.id];
}
And sure enough, null is returned even though the player and id is clearly there.
When I console.log this.user.id I get 4, and gameInterface.gameState.players returns an object with getters for players like so:
{
4: { ... },
5: { ... },
}
Alright, so it does not return the player even though the object and key are being passed correctly...
But I found an extremely strange way to "FIX" this issue: By adding JSON.parse(JSON.stringify(gameState)) like so
get currentPlayer() {
// ...
if (!this.gameInterface.gameState.players[this.user.id]) {
// add this line
JSON.stringify(this.gameInterface.gameState);
return null;
}
return this.gameInterface.gameState.players[this.user.id];
}
It successfully returns the current player for us... Strange no?
My guess is that when we do this, we "bump" the object, Vue notices some change because of this and updates the object correctly. Does anyone know what I'm missing here?
After working on the problem with a friend, I found the underlying issue being a JavaScript-specific one involving Vue's reactive nature.
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
In this section of the documentation, a caveat of Vue's change detection is discussed:
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.
When, in my game run-time, I set players like so:
gameObj.gameState.players[user.id] = {...playerData}
I am adding a new property that Vue has not converted on initialization, and Vue does not detect this change. This is a simple concept I failed to take into account when developing my game run-time.
In order to correctly set a new player, I've decided to use the spread operator to change the entirety of the players object, which Vue is reacting to, and in turn, Vue will detect my player being added like so:
gameObj.gameState.players = {
...gameObj.gameState.players,
[user.id]: {...playerData}
}
Vue also discusses another method called $set, which you can read on the same page.

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.

React code is somehow executing backwards. I'm about to go crazy

Seriously, this is like the double slit experiment of Javascript.
Basically what I have is a series of steps. What I want to happen is that when a user adds a new step to the list, the state for activeStepId gets set to be the newly added step (it gets highlighted in the list of steps).
As you'll see below, I'm listening to changes in my store and triggering the onChange event in the component.
Now, here's the crazy part:
Whenever I do the comparison newState.steps.length > this.state.steps.length to see if the new state has more steps than the previous state, somehow the old state has already been updated to the new state, despite the fact that setState hasn't even been called yet!
Now, I know you're probably thinking there's some other place in my code where I'm updating the state without realizing it. Yeah, I thought that too, but when I removed the call to setState below and then inspected the result, it worked! this.state.steps.length was what it should have been: the old value, not the new one.
I've spent hours on this at this point, and I have literally not a clue what's going on.
componentDidMount: function(){
FlowStepStore.on('change', this.onChange);
},
onChange: function(){
var newState = this.getState();
// If we've just added a step, set it as active
if(newState.steps.length > this.state.steps.length){
newState.activeStepId = newState.steps[newState.steps.length - 1].id;
}
this.setState(newState);
},
getState: function(){
return {
activeStepId: this.activeStepId(),
steps: FlowStepStore.getSteps()
};
},
I also tried doing it a different way, performing the step count comparison by using componentDidUpdate(prevProps, prevState) and comparing prevState against this.state, but I had the exact same problem! Somehow, prevState already equals the current state.
Mind blown.
Help!
#Heap's comment was correct. I didn't realize my state had a direct reference to a variable in the store (JS passes by reference, not by value), which was getting updated prior to setting the component state. Modified my store to return _steps.slice() instead of just _steps and now it works!

Categories

Resources