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

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!

Related

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.

Is it possible to tell exactly what is causing the render function to execute?

I have an app and when I click on a navigation bar it is causing a completely un-related component to render.
I looked at the code and can not find the connection.
It does not break anything but I find it bizarre.
I am aware of React lifecycles and was wondering if how I can troubleshoot further to see what is causing it to render().
I noticed that componentDidUpdate() is called but I don't know why it is being called by React or what is causing it to update. How can I troubleshoot further?
Maybe relevant code is, but maybe not.
componentDidUpdate(prevProps) {
console.log('DEBUG: componentDidUpdate() called', prevProps.Modal);
// Set the state of the form to the data returned from the server.
// This will allow us to PUT / Update the data as this is a controlled form.
// That is the state holds the form input.
// Typical usage (don't forget to compare props) or infinite loop will ocur.
if (this.props.Modal.data !== prevProps.Modal.data) {
// becasue the form did not update but populates we must call update manually for both URLs
this.url.updateURL(this.props.Modal.data.link);
this.img_url.updateURL(this.props.Modal.data.image);
this.setState(this.props.Modal.data);
}
}
prevProps is always the same for each call. i.e. the props do not change.
It is only mounted once and props and state do not change but it keeps updating!
See image:

Issue with update timing for a state in React Component (2 clicks needed instead of 1)

I’m working on a solo project using React and I’ve been stuck on something for the past 2 days…I'm starting and I'm very beginner so it's maybe something very basic, but I'm struggling...
To try to be concise and clear:
I have a searchBar component, that searches through a local database, and returns objects associated with the search keyword. Nothing complicated so far.
Each rendered object has a button that triggers a function onClick. The said function is defined in my App component and is as follow:
changeState(term){
let idToRender=[];
this.state.dealersDb.map(dealer=>{
if(term===dealer.id){
idToRender=[dealer];
}});
let recoToFind=idToRender[0].reco;
recoToFind.map(item=>{
Discogs.search(item).then(response=>{idToRender[0].recoInfo.push(response)})
})
this.setState({
objectToRender: idToRender
});
to explain the above code, what it does is that first, it identifies which object’s button has been clicked on, and send said object to a variable called idToRender. Then, it takes the reco state of that object, and store it to another variable called recoToFind. Then it calls the map() method on recoToFind, make an API request (the discogs() method) for each element of the recoToFind array and push() the results into the recoInfo state of idToRender. So by the end of the function, idToRender is supposed to look like this:
[{
…
…
recoInfo: [{1stAPI call result},{2ndAPI call result}…]
}],
The array contains 1 object having all the states of the object that was originally clicked on, plus a state recoInfo equal to an array made of the results of the several API calls.
Finally, it updates the component’s state objectToRender to idToRender.
And here my problem is, onClick, I do get all the states values of the clicked on object that get rendered on screen (as expected with how I coded the nested components), BUT, the values of the recoInfo are not displayed as expected (The component who’s supposed to render those values is nested in the component rendering the clicked on object other states values). However, they get displayed properly after a SECOND click on the button. So it seems my problem boils down to an state update timing trouble, but I’m puzzled, because this function is calling setState once and I know for a fact that the state is updated because when I click on the button, the clicked on Object details get displayed, but somehow only the recoInfo state seems to not be available yet, but only becomes available on a second click…
Would anyone have a way to solve this issue? :(
It somehow feels like my salvation lies in async/await, but I’m not sure I understand them correctly…
thanks very much in advance for any help!
Is this someting you want to do?
changeState(term) {
let idToRender=[];
this.state.dealersDb.map(dealer=>{
if(term===dealer.id){
idToRender=[dealer];
}});
let recoToFind=idToRender[0].reco;
recoToFind.map(item=>{
Discogs.search(item).then(response=>{
idToRender[0].recoInfo.push(response)
this.setState({
objectToRender: idToRender
});
})
})
}
you can call setState once async call is done and result received.

Does JSON.parse(JSON.stringify(state)) guarantee no mutation of redux state?

I've been tracking down an issue where a redux store correctly updates, but the component does not reflect the change. I've tried multiple ways of making sure I'm not mutating the state, however the issue persists. My reducers commonly take the form:
case 'ADD_OBJECT_TO_OBJECT_OF_OBJECTS': {
newState = copyState(state);
newState.objectOfObjects[action.id] = action.obj;
return newState;
}
For my copyState function, I usually use nested Object.assign() calls, but avoiding errors isn't so straightforward. For testing, to make sure I'm not mutating state, is it correct to use
const copyState = (state) => {
return JSON.parse(JSON.stringify(state));
};
as a guaranteed method of not mutating (redux) state, regardless of how expensive the process is?
If not, are there any other deep copy methods that I can rely on for ensuring I'm not mutating state?
EDIT:
Considering that other people might be having the same issue, I'll relay the solution to the problem I was having.
I was trying to dispatch an action, and then in the line after, I was trying to access data from the store that was updated in the previous line.
dispatchAction(data) // let's say this updates a part of the redux state called 'collection'
console.log(this.props.collection) // This will not be updated!
Refer to https://github.com/reactjs/redux/issues/1543
Yes this does a deep clone, regardless of how expensive it is. The difficulty (as hinted in comment under the question) is to make sure state stays untouched everywhere else.
Since you're asking for other approaches, and that would not fit in a comment, I suggest to take a look at ImmutableJS, which eliminates the issue of tracking state mutation bugs (but might come with its own limitations):
const { Map } = require('immutable')
const map1 = Map({ a: 1, b: 2, c: 3 })
const map2 = map1.set('b', 50)
map1.get('b') // 2
map2.get('b') // 50
Your copyState() function will NOT mutate your state. It is fine. And for others reading this question, it is also fine to directly go
const newState = JSON.parse(JSON.stringify(oldState));
to get a deep copy. Feels unclean, but it is okay to do.
The reason you're not seeing your subscriptions run may be that Redux compares the new state (the output of the reducer function) to the existing state before it copies it. If there are no differences, it doesn't fire any subscriptions. Hence the reason that we never mutate state directly but return a copy -- if we did mutate state, changes would never be detected.
Another reason might be that we don't know what dispatchAction() in your code does exactly. If it is async (maybe because of middleware?) the changes wouldn't be applied until after your console.log() on the next line.

How to ensure I am reading the most recent version of state?

I may be missing something. I know setState is asynchronous in React, but I still seem to have this question.
Imagine following is a handler when user clicks any of the buttons on my app
1. ButtonHandler()
2. {
3. if(!this.state.flag)
4. {
5. alert("flag is false");
6. }
7. this.setState({flag:true});
8.
9. }
Now imagine user very quickly clicks first one button then second.
Imagine the first time the handler got called this.setState({flag:true}) was executed, but when second time the handler got called, the change to the state from the previous call has not been reflected yet -- and this.state.flag returned false.
Can such situation occur (even theoretically)? What are the ways to ensure I am reading most up to date state?
I know setState(function(prevState, props){..}) gives you access to previous state but what if I want to only read state like on line 3 and not set it?
As you rightly noted, for setting state based on previous state you want to use the function overload.
I know setState(function(prevState, props){..}) gives you access to previous state
So your example would look like this:
handleClick() {
this.setState(prevState => {
return {
flag: !prevState.flag
};
});
}
what if I want to only read state like on line 3 and not set it?
Let's get back to thinking why you want to do this.
If you want to perform a side effect (e.g. log to console or start an AJAX request) then the right place to do it is the componentDidUpdate lifecycle method. And it also gives you access to the previous state:
componentDidUpdate(prevState) {
if (!prevState.flag && this.state.flag) {
alert('flag has changed from false to true!');
}
if (prevState.flag && !this.state.flag) {
alert('flag has changed from true to false!');
}
}
This is the intended way to use React state. You let React manage the state and don't worry about when it gets set. If you want to set state based on previous state, pass a function to setState. If you want to perform side effects based on state changes, compare previous and current state in componentDidUpdate.
Of course, as a last resort, you can keep an instance variable independent of the state.
React's philosophy
The state and props should indicate things the components need for rendering. React's render being called whenever the state and props change.
Side Effects
In your case, you're causing a side effect based on user interaction which requires specific timing. In my opinion, once you step out of rendering - you probably want to reconsider state and props and stick to a regular instance property which is synchronous anyway.
Solving the real issue - Outside of React
Just change this.state.flag to this.flag everywhere, and update it with assignment rather than with setState. That way you
If you still have to use .state
You can get around this, uglily. I wrote code for this, but I'd rather not publish it here so people don't use it :)
First promisify.
Then use a utility for only caring about the last promise resolving in a function call. Here is an example library but the actual code is ~10LoC and simple anyway.
Now, a promisified setState with last called on it gives you the guarantee you're looking for.
Here is how using such code would look like:
explicitlyNotShown({x: 5}).then(() => {
// we are guaranteed that this call and any other setState calls are done here.
});
(Note: with MobX this isn't an issue since state updates are sync).

Categories

Resources