How to change more than 1 state in React-easy-state? - reactjs

I am not able to mutate more than 1 state through one function using React-easy-state
I have used batch in my following example and also used mutation separately. However, the code somehow disregards my 2nd state.
piece of the code looks like this :
batch(() => {
// batch is used to test out whether it will trigger both store changes.
store.counter = store.counter + 1;
store.sumNum = 10; // this is never updated/mutated.
});
for reproduction link to sandbox.

You don't have any state called sumNum.
// your store
const myStore = store({
rndNum: "",
counter: 1,
someNum: 2, <--- not sumNum
showRes: false
});
Change it to someNum and it works.

Related

useState non-instantaneous updates break function

I have a sumButtonsDict state variable that stores a dictionary of objects. I've also built a simple addSumButton() function add a new object into the sumButtonsDict:
const [sumButtonsDict, setSumButtonsDict] = useState({})
function addSumButton(sum) {
const dictKey = Object.keys(sumButtonsDict).length
const sumButtonDict = {
sum: sum,
isActive: false
}
setSumButtonsDict(prevState => ({...prevState, [dictKey]: sumButtonDict}))
}
As you can see, the function stores every new dictionary item at a key corresponding to the index it's on (e.g., first item has key 0, second item has key 1...), but the correct key is derived from the count of objects already existing in sumButtonsDict.
When the component mounts, I add 5 new buttons using the following:
useEffect(() => {
addSumButton(10)
addSumButton(25)
addSumButton(50)
addSumButton(100)
addSumButton(250)
}, [])
but only 1 ends up existing in sumButtonsDict. I suspect this is because setState() doesn't update the state variable immediately, and hence when I call Object.keys(sumButtonsDict).length it keeps on returning 0 even though the addSumButton() has run multiple times before.
How can I get around this?
You're already using the function version of setSumButtonsDict to get the previous state, which is the right thing to do. You just need to move the other bits of code into the function too, so the entire calculation uses prevState:
function addSumButton(sum) {
setSumButtonsDict(prevState => {
const dictKey = Object.keys(prevState).length;
const sumButtonDict = {
sum: sum,
active: false,
}
return {...prevState, [dictKey]: sumButtonDict}
});
}

Event handler not picking up correct state value

I am developing a simple Battleships board game and need to increment a state variable when a player clicks on a cell with a valid move.
CodeSandbox: https://codesandbox.io/s/trusting-leakey-6ek9o
All of the code pertaining to this question is found within the following file: >src>GameboardSetup>GameboardSetup.js
The state variable in question is called placedShips. It is designed to keep track of how many ships have been placed onto the board. The variable is initialised to 0 and is supposed to increment up until it reaches a set integer value:
const [placedShips, setPlacedShips] = useState(0);
When the user clicks on the grid, an onClick handler fires. If the cell was a valid cell, then a ship should be placed into an array and the value of placedShips should increment. When the value increments, a new ship is then selected to be placed on a subsequent click. Each new ship has a different length.
Currently, when the user clicks on a valid grid cell, a ship is correctly placed into the array. The issue arises on subsequent valid clicks, whereby the same ship is then placed into the array. This issue is being driven by the onClick handler apparently not receiving an updated state value for placedShips.
While I can see from a { useEffect } hook that the state is in fact increment, the placedShips variable within the event handler I believe is constantly set to 0. Below is how I believe I have validated this issue.
Here is the onClick event handler, containing a console log for the state variable:
const onClickHandler = (e) => {
console.log(placedShips)
let direction = ships[placedShips].direction;
let start = parseInt(e.target.id);
let end = start + ships[placedShips].length - 1;
console.log(playerGameboard.checkIfShipPresent());
if ((playerGameboard.checkValidCoordinates(direction, start, end)) && (!playerGameboard.checkIfShipPresent(direction, start, end))) {
playerGameboard.placeShip(placedShips, direction, start, end);
setPlacedShips(oldValue => oldValue + 1);
}
}
and here is the { useEffect } hook with another console log for the state variable:
useEffect(() => {
console.log(placedShips)
}, [placedShips])
By selecting multiple valid cells, this is the console log output:
GameboardSetup.js:70 0 <--- Event handler console log
GameboardSetup.js:74 false
GameboardSetup.js:143 1 <--- useEffect console log
GameboardSetup.js:70 0
GameboardSetup.js:74 false
GameboardSetup.js:143 2
GameboardSetup.js:70 0
GameboardSetup.js:74 false
GameboardSetup.js:143 3
You can see that the event handler console log always reports placedShips as 0. Whereas the { useEffect } hook shows it incrementing with each successive click.
Apologies for the longwinded question, I have been stuck on this problem for 2 weeks now and haven't made any significant progress with it. Any advice would be hugely appreciated.
Thank you!
Your onClickHandle is not updated when the UI Render. Move it into your return. and it works fine. No need to use humanSetUpGrid. Just get the result from createUiGrid. So cells will be updated when app render.
const createUiGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(i);
}
let counter = -1;
const result = cells.map((cell) => {
counter++;
return (
<div
className="cell"
id={counter}
onClick={onClickHandler}
onMouseOut={onMouseOutHandler}
onMouseOver={onMouseOverHandler}
/>
);
});
return result;
};
<div className="setup-grid">
<Table grid={createUiGrid()} />
</div>

setState accept function vs object

Lets say I have a state that I would like to update:
state = {
description: "",
notes: ""
}
Is there a difference if I update the state with just an object, if I do not need to use prevState?
this.setState({description: "Blue"});
vs
this.setState((prevState)=> ({description: "Blue"}));
I've created the demo to visualize the difference
/* When we doing this state gets update after the for loops runs so,
this.state.count is same for each repeat so even if we expecting to
count increase by 5 its actually increment by 1 */
updateCountByFive = () => {
for (let a = 0; a < 5; a++) {
this.setState({
count: this.state.count + 1
});
}
};
/* In this case by using callback to set the new state,
react is smart enough to pass the latest count value to the callback
event if that new change not rendered */
updateCountByFiveCallback = () => {
for (let a = 0; a < 5; a++) {
this.setState(prevState => {
return {
count: prevState.count + 1
};
});
}
};
So it's a good practice to use callback version when you need to use current state to set the next state, since it will prevent some issues as above
This is what react doc says.
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
there is a nice article here

Pushing element in a cloned object of state adds value to the state attribute instantaneously

I am trying to publish an updated object but not trying to change the state through the following code in react:
addPlaceToSearch = (place) => {
if (place != this.state.search_text) {
var search = $.extend({}, this.state.search);
if (!search.added_places.includes(place)) {
search.added_places.push(place);
PubSub.publish('searchUpdated', search);
}
}
}
Thus, I am using $.extend({}, this.state.search) to clone the object into another variable, modify that particular variable and then publish it. But when I execute the line 5, and I put a breakpoint on line 6, I can see that the this.state.search is also changed with place being pushed in it's added_places key (which I do not want). How is this happening? And how can I prevent it? I have also tried Object.assign({}, this.state.search) instead of $.extend({}, this.state.search).
EDIT
I even tried the most trivial solution, here it is:
addPlaceToSearch = (place) => {
if (place != this.state.search_text) {
if (!this.state.search.added_places.includes(place)) {
var xyz = {};
for (var key in this.state.search) {
xyz[key] = this.state.search[key];
}
xyz.added_places.push(place);
PubSub.publish('searchUpdated', xyz);
}
}
}
Still, the xyz.added_places.push(place) line changes my this.state.search object too! Kindly help me out.
Finally, after two hours of hard work, I figured out the solution by making a deep copy of the original array. I will read about the differences between shallow and deep copy as given in this answer, till then here is the solution:
var xyz = $.extend(true, {}, this.state.search);
xyz.added_places.push(place);
You can do this better without jQuery using Object.assign()
var xyz = Object.assign({}, this.state.search)
xyz.added_places.push(place)

React change state in array (for loop)

I have a State with flights, and I have a slider to change the max price to change visibility of the flight elements.
maxpriceFilter() {
var flightOffer = this.state.flightOffer;
var sliderPrice = this.state.sliderPrice;
for (var i = 0; i < flightOffer.length; i++) {
if( flightOffer[i].price > sliderPrice) {
this.setState(
{[flightOffer[i].hiddenprice] : true}
);
};
}
This code is adding a undefined field with status true in the root of the state though.. I cant find any best practice on this, other then using computed fields. But I cant get the computed field working either..
Could someone please help me out here?
You don't want to do a setState call in a loop, that will have the react component render multiple times. Build a new state object and call the setState once. You also don't want to filter it out by an if statement, but set previous values to hidden:
maxpriceFilter() {
var flightOffer = this.state.flightOffer;
var sliderPrice = this.state.sliderPrice;
var newState = {};
for (var i = 0; i < flightOffer.length; i++) {
newState[flightOffer[i].hiddenprice] = flightOffer[i].price > sliderPrice;
}
this.setState(newState);
// ...
}
If you're still having issues, it could be that the hiddenprice property isn't what you expect? Might need to post your render() function as well.
Instead of doing your looping when you're updating the state, why not just do the switch on render?
You're probably already looping over all flightOffers in render(). So, just add the check there. Inside your render, pass hiddenPrice={offer.price > sliderPrice} as a prop, or use it directly where you control the visibility.
Why? Because the visibility of a specific item in this case is not state. It is a result of the state.

Resources