React update state object using hooks (useState) works, but no rerender - reactjs

I have a weird problem with React hooks (useState).
I have a page which checks my bills and my bank account and checks wether salary is in, and then moves my money to different jars. The page has 2 buttons: check preconditions (salary enough, etc), and run script.
The first one (preconditions) works as expected, also the output (in below code the var currentstate), when I update the state (with setPreconditions) nothing happens.
So, my thought was that state isnt updated, until I found out that when I changed some other field with state (for example salary) the page rerenders and the correct data for currentstate (state preconditions) is displayed.
Why is this happening?
const Bunq = ({auth}) => {
const [accounts, setAccounts] = useState([]);
const [preconditions, setPreconditions] = useState({run: false, succeeded: false, accountsExist: [], balanceSufficient: true, incomeSufficient: true, sparen: null, maandtotaal: 0, balance: null});
const [rekeningen, setRekeningen] = useState([]);
const [salaris, setSalaris] = useState(getLocalStorage('bunq_salaris') || '');
const [eigen_geld, setEigenGeld] = useState(getLocalStorage('bunq_eigen_geld') || '');
const [sparen, setSparen] = useState(0);
const [page_loaded, setPageLoaded] = useState(false);
const [script_running, setScriptRunning] = useState(false);
useEffect(() => {
setLocalStorage('bunq_salaris', salaris);
}, [salaris]);
useEffect(() => {
setLocalStorage('bunq_eigen_geld', eigen_geld);
}, [eigen_geld]);
.......................
const checkPreconditions = () => {
//check
//setScriptRunning(true);
const algemeen_account = getAccountByName("Algemeen");
let maandnummer = (new Date()).getMonth()+1;
let currentstate = preconditions;
currentstate.succeeded = true;
currentstate.maandtotaal = 0;
currentstate.incomeSufficient = true;
currentstate.balanceSufficient = true;
currentstate.balance = algemeen_account.balance.value;
rekeningen.map(rekening => {
currentstate.maandtotaal += rekening["totaal_" + maandnummer];
let foundaccount = getAccountByName(rekening.rekening);
if(foundaccount == null && rekening["totaal_" + maandnummer] > 0){
currentstate.succeeded = false;
currentstate.accountsExist.push(rekening.rekening)
console.log("Rekening bestaat niet: " + rekening.rekening);
}
});
if((parseFloat(algemeen_account.balance.value)) < salaris){
currentstate.balanceSufficient = false;
currentstate.succeeded = false;
}
if((currentstate.maandtotaal + eigen_geld) > salaris){
currentstate.incomeSufficient = false;
currentstate.sparen = 0;
currentstate.succeeded = false;
}else{
currentstate.sparen = (salaris - currentstate.maandtotaal - eigen_geld);
if(currentstate.balanceSufficient){
currentstate.sparen = (currentstate.sparen + (Math.round(algemeen_account.balance.value) - salaris));
}
//console.log(currentstate);
if(currentstate.sparen < 0){
currentstate.sparen = 0;
currentstate.incomeSufficient = false;
currentstate.succeeded = false;
}else{
currentstate.incomeSufficient = true;
}
}
setPreconditions(currentstate);
//setPreconditions('test');
console.log(currentstate, preconditions);
//setScriptRunning(false);
//this.setState({preconditions: currentstate});
}
.........................
return (<div><h1>Bunq</h1>
<DefaultTable data={rekeningen} columns={rekeningColumns} loading={rekeningen.length === 0} pageSize={15}/>
<Form>
<Row>
......................
<Button variant="primary" onClick={() => {checkPreconditions();console.log(preconditions);}} disabled={!page_loaded || script_running}>Controleer</Button>
</Row>
</Form>
<ListGroup>
{JSON.stringify(preconditions)}
{preconditions.balance !== null ?<ListGroup.Item variant="success">Huidig saldo Algemene rekening: {preconditions.balance}</ListGroup.Item> : ""}
{preconditions.accountsExist.map((rek, i) => {return <ListGroup.Item key={i} variant="danger">Rekening {rek} bestaat niet</ListGroup.Item>})}
{preconditions.balanceSufficient === false ? <ListGroup.Item variant="danger">Niet voldoende saldo. Salaris nog niet binnen?</ListGroup.Item> : ""}
{preconditions.incomeSufficient === false ? <ListGroup.Item variant="danger">Niet voldoende inkomen om alle rekeningen te betalen</ListGroup.Item> : ""}
{preconditions.sparen !== null ? <ListGroup.Item variant="success">Er wordt {preconditions.sparen} gespaard</ListGroup.Item> : ""}
</ListGroup>
</div>
);
}

You are mutating your state, so when you call setPreconditions(currentstate) you are updating the state with the exact same object reference, which React will treat as no state being updated.
You can create a copy of the preconditions object instead.
const checkPreconditions = () => {
const algemeen_account = getAccountByName("Algemeen");
let maandnummer = new Date().getMonth() + 1;
let currentstate = { ...preconditions };
// ...
}

Related

How to combine useState hook with switch cases in react

Here is component with the array of items, counters val and spiceValue and rendering block with adding some quantity of spices for example. Every spice has own price, adding or removing by click on plus or minus. How to implement this logic according to best practices of react?
export const SpiceBlock = ({ isCalculated }) => {
const [spiceQuantity, setSpiceQuantity] = useState(0);
var val = 0;
var spiceValue = 0;
Calling useEffect with passed val as argument in any part of this code could not to read val
useEffect((val) => {
setSpiceQuantity();
}, []);
const spicesCount = (event) => {
const direction = event.target.dataset.direction;
const dataSpice = event.target.dataset.spice;
switch (dataSpice) {
case "guarana":spiceValue = 21;break;
case "zhen":spiceValue = 11;break;
case "cinnamon":spiceValue = 33;break;
case "tapioka":spiceValue = 41;break;
default:return false;
}
if (direction === "plus") {
val += spiceValue;
} else if (val - spiceValue > 0) {
val -= spiceValue;
}
};
const spicesArr = [
{ name: "Guarana", data: "guarana" },{ name: "Zhenshen", data: "zhen" },
{ name: "Cinnamon", data: "cinnamon" },{ name: "Tapioka", data: "tapioka" },
];
return (
<div className={`spiceItems ${isCalculated ? "calculated" : ""}`}>
{isCalculated && spicesArr
? spicesArr.map((spice, index) => (
<div className="counter" key={`spice${index}`}>
<button
className="counter__btn"
data-direction="minus"
data-spice={spice.data}
onClick={spicesCount}
>
-
</button>
<label htmlFor="spice" type="text">
{spice.name}
<input
type="text"
name="spice"
value={val}
disabled
className="counter__value"
/>
{spiceQuantity}
</label>
<button
className="counter__btn"
data-direction="plus"
data-spice={spice.data}
onClick={spicesCount}
>
+
</button>
</div>
))
: null}
</div>
);
};
Set val as a state and add it as a dependency to your useEffect. So that every time you set the value of val the setSpiceQuantity function will be triggered
const [val, setVal] = useState(0);
useEffect(() => {
setSpiceQuantity();
}, [val]);
if (direction === "plus") {
setVal(val += spiceValue)
} else if (val - spiceValue > 0) {
setVal(val -= spiceValue)
}
Any variable that is not a state will disappear after rerender, it's better to store variables in the state
const [value, setValue] = useState()
const getValue = type => {
switch (dataSpice) {
case "guarana": return 21;
case "zhen": return 11;
case "cinnamon": return 33;
case "tapioka": return 41;
}
}
const spicesCount = (event) => {
const direction = event.target.dataset.direction;
const dataSpice = event.target.dataset.spice;
const val = getValue(dataSpice);
if (direction === "plus") {
setValue(value => value + spiceValue)
} else if (val - spiceValue > 0) {
setValue(value => value - spiceValue)
}
};
<input
type="text"
name="spice"
value={value}
disabled
className="counter__value"
/>

React update state of a button based on another button

For this project I am currently working on, I need to highlight the button that was clicked on each layer/row. However, the way I have right now it highlights every button that was clicked.
I need something like this:
correct highlighted path
But then when I click on the same row, it does not remove the highlight from the button that I pressed before. How can I update and reset the state of the previous button that was clicked? I tried to use the useRef hook for this but I haven't been successful so far.
wrong highlighted path
EDIT: Added code
This is the code that I have for the component of each row in the website:
function StackRow({ data, partition, level, index, onClick, getInfo, isApp }) {
const classes = useStyles({ level: level });
const rowRef = useRef();
const handleSelectedButtons = (flag, setFlag, btnRef) => {
console.log(rowRef);
};
return (
<Card
key={partition + '_' + index + '_' + level}
className={classes.card}
id={level}
ref={rowRef}
>
{data.map((field) => {
return (
<StackItem
key={partition + '_' + field[0] + '_' + level}
data={field[0]}
info={field[1]}
level={level}
onClick={onClick}
getInfo={getInfo}
isApp={isApp}
handleSelectedButtons={handleSelectedButtons}
rowRef={rowRef}
/>
);
})}
</Card>
);
}
And this is the code I have for each button of the row:
function StackItem({
data,
info,
level,
onClick,
getInfo,
isApp,
handleSelectedButtons,
}) {
const [flag, setFlag] = useState(false);
const btnRef = useRef();
const styleProps = {
backgroundColor: flag ? '#06d6a0' : level % 2 === 0 ? '#22223b' : '#335c67',
};
const classes = useStyles(styleProps);
return (
<Button
ref={btnRef}
isselected={flag.toString()}
key={data}
className={classes.button}
variant="outlined"
onClick={(event) => {
onClick(event, setFlag, btnRef);
handleSelectedButtons(flag, setFlag, btnRef);
getInfo(info, level, isApp);
}}
disableElevation={true}
>
{data}
</Button>
);
}
There are some useless variables and states there because I have been trying all sort of stuff to do this.
EDIT: Added data sample & project structure
Data looks like:
{
application: {
cmake: {
info: str,
versions: {
version_no: {
application: {...}
}
}
},
gcc: {...},
git: {...},
intel: {...},
.
.
.
}
}
The structure of the project is like:
App
L Stack
L StackRow
L StackItem
Where App is the entire application, Stack is the container for everything in the images apart from the search box, StackRow matches one row of the Stack, and StackItem is one item/button from the StackRow.
EDIT: Added Stack component
function Stack({ data, partition, getInfo }) {
const [level, setLevel] = useState(0);
const [cards, setCards] = useState([]);
const [isApp, setIsApp] = useState(true);
const [selected, setSelected] = useState([]);
const [prevLevel, setPrevLevel] = useState(-1);
const cardsRef = useRef();
const handleClick = (event, setFlag, btnRef) => {
let rows = cardsRef.current.childNodes;
let currBtn = event.target.innerText;
let curr;
for (let i = 0; i < rows.length; i++) {
let rowItems = rows[i].childNodes;
for (let j = 0; j < rowItems.length; j++) {
if (currBtn === rowItems[j].textContent) {
curr = rowItems[j].parentElement;
}
}
}
let id;
for (let i = 0; i < rows.length; i++) {
if (curr.textContent === rows[i].textContent) {
id = i;
}
}
if (level === id) {
if (id % 2 === 0) {
setIsApp(true);
if (selected.length === 0) {
setSelected([...selected, data[currBtn].versions]);
} else {
let lastSelected = selected[selected.length - 1];
setSelected([...selected, lastSelected[currBtn].versions]);
}
} else {
let lastSelected = selected[selected.length - 1];
setSelected([...selected, lastSelected[currBtn].child]);
setIsApp(false);
}
setPrevLevel(level);
setLevel(level + 1);
} else {
let newSelected = selected.slice(0, id);
if (id % 2 === 0) {
setIsApp(true);
if (newSelected.length === 0) {
setSelected([...newSelected, data[currBtn].versions]);
} else {
let lastSelected = newSelected[newSelected.length - 1];
setSelected([...newSelected, lastSelected[currBtn].versions]);
}
} else {
let lastSelected = newSelected[newSelected.length - 1];
setSelected([...newSelected, lastSelected[currBtn].child]);
setIsApp(false);
}
setPrevLevel(level);
setLevel(id + 1);
}
setFlag(true);
};
useEffect(() => {
let fields = [];
let lastSelected = selected[selected.length - 1];
if (level % 2 !== 0) {
fields = Object.keys(lastSelected).map((key) => {
let path = lastSelected[key].path;
let module = lastSelected[key].module_name;
let info = 'module: ' + module + ' path: ' + path;
return [key, info];
});
} else {
if (selected.length !== 0)
fields = Object.keys(lastSelected).map((key) => {
let info = lastSelected[key].info;
return [key, info];
});
}
if (fields.length > 0) {
if (level > prevLevel) {
setCards((prevState) => [...prevState, fields]);
} else {
setCards((prevState) => [
...prevState.slice(0, selected.length),
fields,
]);
}
}
}, [selected, level, prevLevel]);
useEffect(() => {
let fields = Object.keys(data).map((key) => {
let info = data[key].info;
return [key, info];
});
setCards([fields]);
setLevel(0);
}, [data]);
useEffect(() => {
setLevel(0);
setPrevLevel(-1);
setSelected([]);
}, [partition]);
if (cards) {
return (
<div ref={cardsRef}>
{cards.map((card, index) => (
<StackRow
data={card}
partition={partition}
level={index}
index={cards.indexOf(card)}
onClick={handleClick}
getInfo={getInfo}
isApp={isApp}
/>
))}
</div>
);
} else return null;
}
EDIT: Added data sample
{
cmake: {
info: "A cross-platform, open-source build system. CMake is a family of tools designed to build, test and package software.",
versions: {
"3.17.3": {
child: {},
module_name: "cmake/3.17.3",
path: "/opt/apps/nfs/spack/var/spack/environments/matador/modules/linux-centos8-x86_64/Core/cmake/3.17.3.lua",
version_no: "3.17.3"
}
}
},
gcc: {
info: "...",
versions: {
"8.4.0": {
child: {
cmake: {...},
cuda: {...},
cudnn: {...},
openmpi: {...},
.
.
.
},
module_name: "...",
path: "...",
version_no: "..."
}
"9.3.0": {...},
"10.1.0": {...}
}
}
}

useEffect() triggers components re-render in one function but not in the other one. Both function DO change state. What am I missing?

It must be something really silly I do wrong here. useEffect() works perfectly with MonthModificatorHandler but not re-render when using dayClick. When dayclick was only adding days re-render worked properly. After adding logic to remove days already in state re-rendering stopped. I can call saveChanges and loadTimeline to fix functionality but if you click few days in a row asynchronous call leads to unexpected results. Thanks for your time.
export default function DatePicker(props) {
const classes = useStyles();
const theme = useTheme();
const [monthModificator, setMonthModificator] = React.useState(0);
const [monthMatrix, setMonthMatrix] = React.useState([]);
const [selectedDates, setSelectedDates] = React.useState([]);
const MonthModificatorHandler = value => {
setMonthModificator(monthModificator + value);
};
const dayClick = day => {
let data = selectedDates;
let addDay = true;
if (data.length === 0) {
data.push(day);
} else {
data.map((date, index) => {
if (day.equals(date)) {
data.splice(index, 1);
addDay = false;
}
});
if (addDay) {
data.push(day);
}
}
setSelectedDates(data);
// saveChanges();
// loadTimeline();
};
let now = DateTime.local().plus({ months: monthModificator });
let firstDayOfFirstWeek = now.startOf("month").startOf("week");
let lastDayOfLasttWeek = now.endOf("month").endOf("week");
let monthToDisplay = Interval.fromDateTimes(
firstDayOfFirstWeek,
lastDayOfLasttWeek
);
function loadTimeline() {
axios.get(`/timeline`).then(response => {
let selectedDays = [];
response.data.map(date => {
selectedDays.push(DateTime.fromISO(date));
});
setSelectedDates(selectedDays);
});
}
useEffect(() => {
let load = true;
if (load) {
loadTimeline();
load = false;
}
var matrix = [];
for (let v = 0; v < monthToDisplay.length("day"); v++) {
matrix.push(firstDayOfFirstWeek.plus({ day: v }));
}
setMonthMatrix(matrix);
}, [selectedDates, monthModificator]);
function saveChanges() {
let arrayOfDataObjects = selectedDates;
let arrayOfDataStrings = arrayOfDataObjects.map(singleDataObject => {
return (
"," +
JSON.stringify(singleDataObject.toISODate()).replaceAll('"', "") // extra quotes removed
);
});
axios.post(`/timeline`, {
timeline: arrayOfDataStrings
});
}
return (
<Grid container justify="space-around">
<Button onClick={() => MonthModificatorHandler(1)}>+</Button>
<Button onClick={() => MonthModificatorHandler(-1)}>-</Button>
<Card className={classes.root}>
{monthMatrix.map((day, index) => {
let color = "secondary";
selectedDates.map(workingDay => {
if (day.equals(workingDay)) {
color = "primary";
}
});
return (
<Button
color={color}
variant="contained"
onClick={() => dayClick(day)}
className={classes.days}
key={index}
>
{day.day}
</Button>
);
})}
</Card>
<Button onClick={() => saveChanges()}>Save Changes</Button>
<Button onClick={() => loadTimeline()}>Update Changes</Button>
</Grid>
);
}
Maybe the problem is that you compute new state from previous state. It should be done with callback https://reactjs.org/docs/hooks-reference.html#functional-updates
Try something like
const dayClick = day => setSelectedDates((_data) => {
let data =[..._data];
let addDay = true;
if (data.length === 0) {
data.push(day);
} else {
data.map((date, index) => {
if (day.equals(date)) {
data.splice(index, 1);
addDay = false;
}
});
if (addDay) {
data.push(day);
}
}
return data
})
Answered by Kostya Tresko, thank you. On top of that, another mistake was in the hook itself. The way I loaded data caused re rending loop.
if (load) {
loadTimeline();
load = false;
}
DO NOT DO THAT

State gets Updated only when we click on the button twice and not once by using Recat Hooks

I need to show only the highest value when I click on the button.
But here the state is getting updated only when we click on the
button twice and not once.
Can we do this using ES6 features like the filter method? Here I did it by using a for loop:
const [dataObject, setdataObject] = useState([{}]);
const [filterData, setfilterData] = useState({});
const showMillions = () => {
// //console.log(dataObject);
let x = 0;
let name = '';
for (let i = 0; i < dataObject.length; i++) {
// console.log(dataObject);
if (dataObject[i].money > x) {
x = dataObject[i].money;
name = dataObject[i].name;
}
}
setfilterData(name);
for (let i = 0; i < dataObject.length; i++) {
if (filterData === dataObject[i].name) {
setdataObject([dataObject[i]]);
}
}
};
<div><button className="btn btn-dark" onClick={showMillions} >Show Millionaires</button></div>
You had to click the button twice because the setfilterData(name) is asynchronous. You can either use useEffect or use the name instead of filterData in the last for loop.
const [dataObject, setdataObject] = useState([{}]);
const [filterData, setfilterData] = useState({});
useEffect(() => {
// using filter
setDataObject(dataObject.filter((item) => item.name === filterData));
}, [filterData]);
const showMillions = () => {
// //console.log(dataObject);
let x = 0;
let name = '';
for (let i = 0; i < dataObject.length; i++) {
// console.log(dataObject);
if (dataObject[i].money > x) {
x = dataObject[i].money;
name = dataObject[i].name;
}
}
setfilterData(name);
}
};
<div><button className="btn btn-dark" onClick={showMillions} >Show Millionaires</button></div>

Running a Function on Click Returns Stale State Values React Hooks

I am refactoring a class component into a functional component with React Hooks in an app that runs a specific function on click. The function references state values, but the state values in the function are stale, and it causes the app to crash.
I've seen similar questions on StackOverflow, but most of the onClick functions do only one thing, so their use of useRef or useCallback seem much easier to implement. How can I ensure that the checkAnswer function is using updated state values?
const Find = props => {
const [currentCountry, setCurrentCountry] = useState(null)
const [guesses, setGuesses] = useState(null)
const [questions, setQuestions] = useState([])
EDIT
The setCurrentCountry hook is called in the takeTurn function, which runs at the start of the game.
const takeTurn = () => {
!props.isStarted && props.startGame();
let country = getRandomCountry();
console.log(country)
setGuesses(prevGuess => prevGuess + 1)
setCurrentCountry(country)
console.log('setting currentCountry')
getAnswers(country)
let nodes = [...(document.getElementsByClassName("gameCountry"))];
nodes.forEach( node => {
node.removeAttribute("style")
})
if(questions && questions.length === 10){
console.log('opening modal')
props.handleOpen();
// alert("Congrats! You've reached the end of the game. You answered " + props.correct + " questions correctly and " + props.incorrect + " incorrectly.\n Thanks for playing");
console.log('ending game')
props.gameOver && endGame();
}
const getAnswers = (currentCountry) => {
console.log(currentCountry)
let answerQuestions;
if(questions){
answerQuestions = [...questions]
}
let question = {};
question['country'] = currentCountry;
question['correct'] = null;
let answers = [];
currentCountry && console.log(currentCountry.name);
console.log(currentCountry)
currentCountry && answers.push({
name: currentCountry.name.split(';')[0],
correct: 2
});
console.log(answers)
answerQuestions.push(question);
setQuestions(answerQuestions)
}
const checkAnswer = (e, country) => {
let checkquestions = questions;
let question = checkquestions.find(question => question.country === currentCountry);
let checkguesses = guesses;
console.log(e)
console.log(country)
console.log(currentCountry)
if(!props.isStarted){
return
}
if((country === currentCountry.name || country === currentCountry.name) || guesses === 4){
props.updateScore(3-guesses);
console.log(question);
if(guesses === 1){
question['correct'] = true;
}
checkguesses = null;
setTimeout(() => takeTurn(), 300);
} else {
question['correct'] = false;
checkguesses ++
if(guesses === 3){
getCountryInfo(e, currentCountry.name);
}
}
setGuesses(checkguesses)
props.handlePoints(questions);
}
The rendered data with the onClick:
<Geographies geography={data}>
{(geos, proj) =>
geos.map((geo, i) =>
<Geography
data-idkey={i}
onClick={((e) => checkAnswer(e, geo.properties.NAME_LONG))}
key={i}
geography={geo}
projection={proj}
className="gameCountry"
/>
)
}
</ Geographies>
</ZoomableGroup>
The app stalls because the state values for currentCountry are still being read as null.

Resources