useState() is not updating state from event handler? - reactjs

I'm trying to recreate an old flash game in React. The object of the game is to press a button down for a certain length of time.
This is the old game:
http://www.zefrank.com/everysecond/index.html
Here is my new React implementation:
https://codesandbox.io/s/github/inspectordanno/every_second
I'm running into a problem. When the mouse is released, I calculate the amount of time between when the button was pressed and when it was released, using the Moment.js time library. If the timeDifference between the onMouseDown and onMouseUp event is within the targetTime, I want the game level to increase and the targetTime to increase as well.
I'm implementing this logic in the handleMouseUp event handler. I'm getting the expected times printed to the screen, but the logic isn't working. In addition, when I console.log() the times, they are different than the ones being printed to the screen. I'm fairly certain timeHeld and timeDifference aren't being updated correctly.
Initially I thought there was a problem with the way I was doing the event handler and I need to use useRef() or useCallback(), but after browsing a few other questions I don't understand these well enough to know if I have to use them in this situation. Since I don't need access to the previous state, I don't think I need to use them, right?
The game logic is in this wrapper component:
import React, { useState } from 'react';
import moment from 'moment';
import Button from './Button';
import Level from './Level';
import TargetTime from './TargetTime';
import TimeIndicator from './TimeIndicator';
import Tries from './Tries';
const TimerApp = () => {
const [level, setLevel] = useState(1);
const [targetTime, setTargetTime] = useState(.2);
const [isPressed, setIsPressed] = useState(false);
const [whenPressed, setPressed] = useState(moment());
const [whenReleased, setReleased] = useState(moment());
const [tries, setTries] = useState(3);
const [gameStarted, setGameStarted] = useState(false);
const [gameOver, setGameOver] = useState(false);
const timeHeld = whenReleased.diff(whenPressed) / 1000;
let timeDifference = Math.abs(targetTime - timeHeld);
timeDifference = Math.round(1000 * timeDifference) / 1000; //rounded
const handleMouseDown = () => {
!gameStarted && setGameStarted(true); //initialize game on the first click
setIsPressed(true);
setPressed(moment());
};
const handleMouseUp = () => {
setIsPressed(false);
setReleased(moment());
console.log(timeHeld);
console.log(timeDifference);
if (timeDifference <= .1) {
setLevel(level + 1);
setTargetTime(targetTime + .2);
} else if (timeDifference > .1 && tries >= 1) {
setTries(tries - 1);
}
if (tries === 1) {
setGameOver(true);
}
};
return (
<div>
<Level level={level}/>
<TargetTime targetTime={targetTime} />
<Button handleMouseDown={handleMouseDown} handleMouseUp={handleMouseUp} isGameOver={gameOver} />
<TimeIndicator timeHeld={timeHeld} timeDifference={timeDifference} isPressed={isPressed} gameStarted={gameStarted} />
<Tries tries={tries} />
{gameOver && <h1>Game Over!</h1>}
</div>
)
}
export default TimerApp;
If you want to check the whole app please refer to the sandbox.

If you update some state inside a function, and then try to use that state in the same function, it will not use the updated values. Functions snapshots the values of state when function is called and uses that throughout the function. This was not a case in class component's this.setState, but this is the case in hooks. this.setState also doesn't updates the values eagerly, but it can update while in the same function depending on a few things(which I am not qualified enough to explain).
To use updated values you need a ref. Hence use a useRef hook. [docs]
I have fixed you code you can see it here: https://codesandbox.io/s/everysecond-4uqvv?fontsize=14
It can be written in a better way but that you will have to do yourself.
Adding code in answer too for completion(with some comments to explain stuff, and suggest improvements):
import React, { useRef, useState } from "react";
import moment from "moment";
import Button from "./Button";
import Level from "./Level";
import TargetTime from "./TargetTime";
import TimeIndicator from "./TimeIndicator";
import Tries from "./Tries";
const TimerApp = () => {
const [level, setLevel] = useState(1);
const [targetTime, setTargetTime] = useState(0.2);
const [isPressed, setIsPressed] = useState(false);
const whenPressed = useRef(moment());
const whenReleased = useRef(moment());
const [tries, setTries] = useState(3);
const [gameStarted, setGameStarted] = useState(false);
const [gameOver, setGameOver] = useState(false);
const timeHeld = useRef(null); // make it a ref instead of just a variable
const timeDifference = useRef(null); // make it a ref instead of just a variable
const handleMouseDown = () => {
!gameStarted && setGameStarted(true); //initialize game on the first click
setIsPressed(true);
whenPressed.current = moment();
};
const handleMouseUp = () => {
setIsPressed(false);
whenReleased.current = moment();
timeHeld.current = whenReleased.current.diff(whenPressed.current) / 1000;
timeDifference.current = Math.abs(targetTime - timeHeld.current);
timeDifference.current = Math.round(1000 * timeDifference.current) / 1000; //rounded
console.log(timeHeld.current);
console.log(timeDifference.current);
if (timeDifference.current <= 0.1) {
setLevel(level + 1);
setTargetTime(targetTime + 0.2);
} else if (timeDifference.current > 0.1 && tries >= 1) {
setTries(tries - 1);
// consider using ref for tries as well to get rid of this weird tries === 1 and use tries.current === 0
if (tries === 1) {
setGameOver(true);
}
}
};
return (
<div>
<Level level={level} />
<TargetTime targetTime={targetTime} />
<Button
handleMouseDown={handleMouseDown}
handleMouseUp={handleMouseUp}
isGameOver={gameOver}
/>
<TimeIndicator
timeHeld={timeHeld.current}
timeDifference={timeDifference.current}
isPressed={isPressed}
gameStarted={gameStarted}
/>
<Tries tries={tries} />
{gameOver && <h1>Game Over!</h1>}
</div>
);
};
export default TimerApp;
PS: Don't use unnecessary third party libraries, especially big ones like MomentJs. They increase your bundle size significantly. Use can easily get current timestamp using vanilla js. Date.now() will give you current unix timestamp, you can subtract two timestamps to get the duration in ms.
Also you have some unnecessary state like gameOver, you can just check if tries > 0 to decide gameOver.
Similarly instead of targetTime you can just use level * .2, no need to additional state.
Also whenReleased doesn't needs to be a ref or state, it can be just a local variable in mouseup handler.

State updaters can take a value indicating the new state, or a function that maps the current state to a new state. The latter is the right tool for the job when you have state that depends on mutations of itself.
This should work if you update places in the code where you use the pattern
[value, setValue ] = useState(initial);
...
setValue(value + change);
to
[value, setValue ] = useState(initial);
...
setValue((curValue) => curValue + change);
For example,
if (timeDifference <= .1) {
setLevel((curLevel) => curLevel + 1);
setTargetTime((curTarget) => curTarget + .2);
} else if (timeDifference > .1 && tries >= 1) {
setTries((curTries) => {
const newTries = curTries - 1;
if (newTries === 1) {
setGameOver(true);
}
return newTries;
});
}

I think there are two subtle things going on here:
When you call a setState method (e.g. setRelease(moment())) the value of the associated variable (e.g. whenReleased) does not update immediately. Instead it queues a re-render, and only once that render happens will the value be updated.
The event handlers (e.g. handleMouseUp) are closures. Meaning they capture the values from the parent scope. And again therefor are only updated on a re-render. So, when handleMouseUp runs, timeDifference (and timeHeld) will be the value that was calculated during the last render.
The changes you therefore need to make are:
Move the calculation of timeDifference inside your handleMouseUp event handler.
Instead of using whenReleased in your timeDifference calculation, you need to use a local variable set to moment() (You can also set whenReleased via setReleased, but that value won't be available to you inside your event handler).
const handleMouseUp = () => {
const released = moment();
setIsPressed(false);
setReleased(released);
const timeHeld = released.diff(whenPressed) / 1000;
const timeDifference = Math.round(1000 * Math.abs(targetTime - timeHeld)) / 1000;
console.log(timeHeld);
console.log(timeDifference);
if (timeDifference <= .1) {
setLevel(level + 1);
setTargetTime(targetTime + .2);
} else if (timeDifference > .1 && tries >= 1) {
setTries(tries - 1);
}
if (tries === 1) {
setGameOver(true);
}
};

When the mouse is released, I calculate the amount of time between when the button was pressed and when it was released...
This is not true ... but can be ... just move the time difference calulations into handleMouseUp()
... also - you don't need whenReleased

Related

Call function when time changes in react

Being new to react , this all is really confusing and new to me , so I apologise if I'm making some obvious oversight.
Im making a stopwatch and implementing the seconds for starters. However; Im confused as to how i'll implement the on display seconds number to update when each second passes.
This is what I'm doing right now
function App() {
const [time , updateTime] = React.useState(0);
var startsec = 0;
//UpdateTime should get triggered when next second passes
const UpdateTime = () => {
//Update time variable with the new seconds elapsed
}
//Should run every second or something
const CheckTimeUpdation = () => {
currentsec = Math.floor(Date.now() / 1000.0);
console.log(currentsec);
if(currentsec > startsec){
UpdateTime(currentsec-startsec);
}
}
const GetStartTime = () => {
startsec = Math.floor(Date.now() / 1000.0);
}
//Clock component just gets a number and displays it on the screen
return (<div className = "App">
<Clock timerSeconds= {time}/>
<div>
<button onClick={GetStartTime}></button>
</div>
</div>);
}
export default App;
Date.now() function gets the miliseconds passed since 1970 (hence the division by 1000 to make them into seconds) and I find the difference between when the button was clicked and current one and passs that to the time component to display.
How do I make the CheckTimeUpdation function run every second or so?
What you want is the setInterval() method (see: https://developer.mozilla.org/en-US/docs/Web/API/setInterval)
However your code so far has some issues:
On the button click, getStartTime runs and it updates the value of startsec. Firstly, this does not cause the component to re-render, and so the component will not update and you will see nothing changing on your screen. Also, if you did get your component to re-render, you will notice that startsec will be 0 again on the next re-render, so re-assigning startsec like how you did likely doesn't do what you want it to. If you want to persist values between rerenders, you can use useState (https://reactjs.org/docs/hooks-reference.html#usestate) or useRef (https://reactjs.org/docs/hooks-reference.html#useref).
Now i'm assuming you want to start the timer when the button is clicked. What you need is to start the interval (via setInterval) on the button click, and update time every 1000ms.
You'd also want to clear the interval once you don't need it anymore, using the clearInterval() method. You'll need to save the id returned from setInterval() initially in order to clear it later.
Below I have written a short example using this idea with setInterval on button click to help you:
import { useState, useRef } from "react";
export default function App() {
const [timerState, setTimerState] = useState("paused");
const [timeElapsed, setTimeElapsed] = useState(0);
const intervalId = useRef(0);
const isRunning = timerState === "running";
const isPaused = timerState === "paused";
const onStartTimer = () => {
intervalId.current = setInterval(() => {
setTimeElapsed((time) => time + 1);
}, 1000);
setTimerState("running");
};
const onStopTimer = () => {
clearInterval(intervalId.current);
intervalId.current = 0;
setTimerState("paused");
};
const onClearTimer = () => {
setTimeElapsed(0);
};
return (
<div className="App">
<h1>{timeElapsed}</h1>
{isPaused && <button onClick={onStartTimer}>Start timer</button>}
{isRunning && <button onClick={onStopTimer}>Stop timer</button>}
{!!timeElapsed && <button onClick={onClearTimer}>Clear timer</button>}
</div>
);
}
You can use setInterval in the GetStartTime Function.
const GetStartTime = () => {
startsec = Math.floor(Date.now() / 1000.0);
setInterval(() => CheckTimeUpdation(), 1000);
}

Pomodoro Timer REACTJS not working - useState not updating

I'm quite new to ReactJS and have been working on a Pomodoro Timer. How it works is that whenever the "Work Timer" reaches zero, it switches to the "Break Timer".
const [secondsLeft, setSecondsLeft] = useState(newTimer.work);
However,
this line: setSecondsLeft(nextSeconds); // This does not update secondsLeft
under useEffect, does not update the secondsLeft which results in the timer for the break to display wrongly.
Could use some advice to make this work.
Thanks!
import {
Button,
Heading,
VStack,
Stack,
HStack,
Box,
CircularProgress,
Text,
CircularProgressLabel,
} from '#chakra-ui/react';
import { useState, useRef, useContext, useEffect } from 'react';
import { SettingsContext } from '../helpers/SettingsContext';
import PomodoroSettings from './PomodoroSettings';
// Pomodoro
const Pomodoro = () => {
//Timer
const { newTimer, setNewTimer } = useContext(SettingsContext);
const [isPaused, setIsPaused] = useState(true);
const [secondsLeft, setSecondsLeft] = useState(newTimer.work);
const secondsLeftRef = useRef(secondsLeft);
const isPausedRef = useRef(isPaused);
const modeRef = useRef(newTimer.active);
function tick() {
secondsLeftRef.current--;
setSecondsLeft(secondsLeftRef.current);
}
useEffect(() => {
function switchMode() {
const nextMode = modeRef.current === 'work' ? 'break' : 'work';
const nextSeconds =
(nextMode === 'work' ? newTimer.work : newTimer.short) * 60;
setNewTimer({
work: newTimer.work,
short: newTimer.short,
long: newTimer.long,
active: nextMode,
});
modeRef.current = nextMode;
console.log(nextMode);
console.log('Next: ' + nextSeconds);
setSecondsLeft(nextSeconds); // This does not update secondsLeft
console.log('SecondsLeft: ' + secondsLeft);
secondsLeftRef.current = nextSeconds;
}
secondsLeftRef.current = newTimer.work * 60;
setSecondsLeft(secondsLeftRef.current);
const interval = setInterval(() => {
if (isPausedRef.current) {
return;
}
if (secondsLeftRef.current === 0) {
return switchMode();
}
tick();
}, 1000);
return () => clearInterval(interval);
}, [newTimer, setNewTimer]);
const totalSeconds =
newTimer.active === 'work' ? newTimer.work * 60 : newTimer.short * 60;
const percentage = Math.round((secondsLeft / totalSeconds) * 100);
const minutes = Math.floor(secondsLeft / 60);
let seconds = secondsLeft % 60;
if (seconds < 10) seconds = '0' + seconds;
// Start / Pause
const handleButton = (e) => {
e.preventDefault();
if (isPaused) {
setIsPaused(!isPaused);
isPausedRef.current = false;
} else {
setIsPaused(!isPaused);
isPausedRef.current = true;
}
};
// console.log(percentage)
// console.log(newTimer.active)
// console.log(secondsLeft)
// console.log(totalSeconds)
return (
<div>
<Heading as='h4' size='md'>
{' '}
Pomodoro{' '}
</Heading>
<VStack>
<HStack>
<Button variant='ghost'>Pomodoro</Button>
<Button variant='ghost'>Short Break</Button>
<Button variant='ghost'>Long Break</Button>
</HStack>
<CircularProgress
value={percentage}
color={newTimer.active === 'work' ? 'red.400' : 'green.400'}
size='200px'
thickness='10px'
>
<CircularProgressLabel>
<Text fontSize='3xl'>{minutes + ':' + seconds}</Text>
</CircularProgressLabel>
</CircularProgress>
<HStack>
<Button variant='outline' onClick={handleButton}>
{isPaused ? <div> START </div> : <div> STOP </div>}
</Button>
<PomodoroSettings />
</HStack>
</VStack>
</div>
);
};
export default Pomodoro;
useState uses array const [value, setValue] like this. It might be just an error in this declaration of newTimer.
There is too much going on in one eseEffect(). The initial value of newTimer.work is not modified so each re-render will set secondsLeftRef to the same initial value.
Since useEffect()updates a context object, the component will re-render on every iteration, setting secondsLeftRef to its initial value.
Not sure where the problem is but few things I noticed that you can try to fix or improve in your code -
Do not use or assign context or props value in useState as you are doing it for secondsLeft, reason is that useState is supposed to run once when component gets rendered first time and you might not get value for newTimer.work on very first render.
For this you can use useEffect and newTimer in dependency array.
const [secondsLeft, setSecondsLeft] = useState(newTimer.work);
You are trying to log secondsLeft immediatly after calling setSecondsLeft this will not give you correct value because setting state is asynchronous in react so it will not be available on next line after calling set state.
setSecondsLeft(nextSeconds);
console.log('SecondsLeft: ' + secondsLeft);
One last thing I noticed looking at your code is that it's possible that you might be registering multiple setInterval because below line is responsible for clearing interval only when component gets unmount but your useEffect will get called multiple times based on its dependencies.
return () => clearInterval(interval);
Try to debug your code for above 3rd point and if that's the problem you
can try clearing your interval in start of useEffect or add
some conditions to make sure it will get register once to get
expected results.
Note - I have not tried executing your code, but let me know if this helps or if I can improve my answer in any way.

Redux useDispatch Hook on click event of nodeListOf<HTMLElement>

I have a component that renders a grid. I'm trying to count the moves made (onclick of each grid box).
But when I include dispatch on the eventListener it returns an error. The moveCharacter function is supposed to move the character around those boxes and its working well. I just need a way to be able to count the moves made (onclick of each box) and store in general state to use in another component.
function GridBoxes():JSX.Element{
const gridValue: number = useSelector<IStateProps, IStateProps["grid"]>((state)=> state.grid);
const totalMoves: number = useSelector<IStateProps, IStateProps["totalMoves"]>((state)=> state.totalMoves);
const history = useHistory();
const dispatch = useDispatch();
useEffect(()=>{
const boxElements = document.querySelectorAll('.box');
boxElements.forEach(element => {
element.addEventListener('click', (e)=>{
moveCharacter(element.id, getCharacterPosition(boxElements));
dispatch({type: "COUNT_MOVES", totalMoves: totalMoves + 1});
console.log("moves");
});
});
});
useEffect(()=> {
let t = setInterval(()=> {
const timeSpent = document.getElementById("time-spent");
const indicator = document.getElementById("indicator");
let countDown = Number(timeSpent?.innerHTML);
countDown = countDown - 1;
let timeTakenPercent = ((gridValue*3) - countDown) / (gridValue*3) * 100;
dispatch({type:"SET_TIME", payload: (gridValue*3)-countDown});
(indicator as any).style.width = timeTakenPercent+"%";
(timeSpent as any).innerHTML = countDown.toString().length < 2 ? countDown.toString().padStart(2,"0") : countDown;
if(countDown < 1){
clearInterval(t);
history.push("/over");
play("https://freesound.org/data/previews/175/175409_1326576-lq.mp3");
}
}, 1000);
});
const [emptyBox, characterBox, foodBox]: string[] = ['<div class="box"></div>',`<div class="box"><img src=${assets.character} /></div>`, `<div class="box"><img src=${assets.food} /></div>`];
const generatedGrids: string[][] = gridPattern({grid: gridValue, box:emptyBox, character: characterBox, food: foodBox});
return (
<>
{generatedGrids.map((box, i)=> {
box = setElementId(box,i);
return <div key={i} className="col">{ReactHtmlParser(box.join(" "))}</div>;
})}
</>
);
}
export default GridBoxes;
Error gotten
The error does not really come from Redux, but from some manual DOM manipulation you are doing.
Your dispatch call only surfaces it: when calling dispatch, redux state will change which will trigger a React rerender - while you are manually fiddling around with the DOM and the two things collide.
My question is: why do you do all this? The whole point of React is that is builds the DOM for you and attaches event handlers for you. At no point should you be using something like react-html-parser, manually concatenate html strings, manually call addEventListener, modify innerHTML, style or anything else on DOM elements.
Let React build your DOM for you and this error will go away.

React multiple useState hooks in event handler behaving in an erroneous way

I have coded a simple React application that tracks feedback, the whole code for the application is below:
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
const App = () => {
// save clicks of each button to its own state
const [good, setGood] = useState(0)
const [neutral, setNeutral] = useState(0)
const [bad, setBad] = useState(0)
const [all, setAll] = useState(0)
const [average, setAverage] = useState(0)
const [positive, setPositive] = useState(0)
const handleGood = () => {
setGood(good + 1)
setAll(all + 1)
setAverage((average + 1) /(all))
setPositive(good/all)
}
const handleNeutral = () => {
setNeutral(neutral + 1)
setAll(all + 1)
setAverage((average) / (all))
}
const handleBad = () => {
setBad(bad + 1)
setAll(all + 1)
setAverage((average - 1) / (all))
}
return (
<div>
<h1>give feedback</h1>
<button onClick={handleGood}>good</button>
<button onClick={handleNeutral}>neutral</button>
<button onClick={handleBad}>bad</button>
<h3>statistics</h3>
<div>good {good}</div>
<div>neutral {neutral}</div>
<div>bad {bad}</div>
<div>all {all}</div>
<div>average {average}</div>
<div>positive {positive}</div>
</div>
)
}
ReactDOM.render(<App />,
document.getElementById('root')
)
From the initial state if I click the "good" button once, the AVERAGE comes out to Infinity and POSTIVE comes out to Nan, I'm really confused why the application is behaving like this, is this because I'm using multiple setState methods in the click event handlers, and they are synchronous functions? Is there an important concept that I'm missing here when using useState hooks.
and they are synchronous functions
Sort of - they don't return promises, but when you do:
setAll(all + 1)
setAverage((average) / (all))
all won't have updated until the next render cycle. What you need to do is more like:
const newAll = all + 1
setAll(newAll);
setAverage(average/newAll);
if you want to use the updated value immediately after setting it.

How to synchronously change state within useState

Hey guys I am new to using React Hooks but couldn't find it on google directly. I am attempting to nest setState callback so that the state can update synchronously but haven't had success as I get undefined values. The values within the state are reliant on other values within my state so I would ideally like it to run synchronously. I tried nesting it below, which works when it is a class component and use this.setState and use the callback, but doesn't work when I attempt to use react hooks within a functional class.
Here is my code:
const [state, setState] = useState({numCols: 0, numRows: 0, cardWidth: 0, cardHeight: 0, containerWidth: 0});
const {
sheetList,
sheetsTotalCount,
sheetsMetadata,
id,
projectId,
onEditButtonClick,
handleInfiniteLoad,
sheetsAreLoading,
classes,
permissions,
onItemClick,
} = props;
const setCardSize = ({ width }) {
setState({
containerWidth: width > 0 ? width : defaultWidth
},() => {
setState({numCols: Math.max(Math.floor(state.containerWidth / baseThumbnailWidth), 1) }, () => {
setState({numRows: Math.ceil(sheetList.size / state.numCols)}, () => {
setState({cardWidth: Math.floor(state.containerWidth / state.numCols - 2 * marginBetweenCards)}, () => {
setState({cardHeight: Math.round(state.cardWidth * thumbnailProportion)});
});
});
});
});
}
Ideally I would like the containerWidth variable to update first, then the numCols variable, then the cardWidth, then the cardHeight. Is there any way to do this synchronously so I don't get an undefined value?
Seeing as you're calculating a load of variables that are dependant upon another, and you want the state to all update at the same time, why not split the calculations out and set state once at the end? Much more readable, and only need one setState call.
const setCardSize = ({ width }) => {
const containerWidth = width > 0 ? width : defaultWidth;
const numCols = Math.max(Math.floor(containerWidth / baseThumbnailWidth), 1,);
const numRows = Math.ceil(sheetList.size / numCols);
const cardWidth = Math.floor(containerWidth / numCols - 2 * marginBetweenCards);
const cardHeight = Math.round(cardWidth * thumbnailProportion);
setState({ containerWidth, numCols, numRows, cardWidth, cardHeight });
};
To answer the actual question though, if you want to cause the state update of one variable to immediately (or "synchronously" as you put it) update another state variable, then use useEffect.
You just give useEffect two parameters: a function to run every time a dependant variable changes, and then an array of those variables to keep an eye on.
It is cleaner (and faster, less bug-prone, and generally recommended for functional components) for each state variable to have its own useState, rather than just one large object, which I have also done here.
const [containerWidth, setContainerWidth] = useState(0);
const [numCols, setNumCols] = useState(0);
const [numRows, setNumRows] = useState(0);
const [cardWidth, setCardWidth] = useState(0);
const [cardHeight, setCardHeight] = useState(0);
const setCardSize = ({ width }) => setContainerWidth(width > 0 ? width : defaultWidth)
useEffect(() => setNumCols(Math.max(Math.floor(containerWidth / baseThumbnailWidth), 1)) , [containerWidth])
useEffect(() => setNumRows(Math.ceil(sheetList.size / numCols)), [numCols])
useEffect(() => setCardWidth(Math.floor(containerWidth / numCols - 2 * marginBetweenCards)), [containerWidth])
useEffect(() => setCardHeight(Math.round(cardWidth * thumbnailProportion)), [cardWidth])
I'm sort of confused on what you want to achieve. But don't forget, unlike a class you have to set all properties a in state each time.
setState({numCols: Math.max(Math.floor(state.containerWidth / baseThumbnailWidth), 1) }
This code will replace all your state with just numCols. You want the rest of state in there like this, now only numCols will change, everything else will be the same.
setState({...state, numCols: Math.max(Math.floor(state.containerWidth / baseThumbnailWidth), 1) }
Next thing to remember is if you want to change state multiple time in one render use this form:
setState(oldState => {...oldState, newValue: 'newValue'});
This will allow for multiple updates to state in one render with the last value set instead of on the last render. For example:
const [state, setState] = useState(0); // -> closed on this state's value!
setState(state + 1);
setState(state + 1);
setState(state + 1); //State is still 1 on next render
// because it is using the state which happened on the last render.
// When this function was made it "closed" around the value 0
// (or the last render's number) hence the term closure.
vs this:
const [state, setState] = useState(0);
setState(state => state + 1);
setState(state => state + 1);
setState(state => state + 1); //State is 3 on next render.
But why not just calculate the values synchronously?
const setCardSize = (width) => {
const containerWidth = width > 0 ? width : defaultWidth;
const numCols = Math.max(Math.floor(containerWidth / baseThumbnailWidth), 1);
const numRows = Math.ceil(sheetList.size / numCols);
const cardWidth = Math.floor(containerWidth / numCols - 2 * marginBetweenCards);
const cardHeight = Math.round(cardWidth * thumbnailProportion);
setState({containerWidth, numCols, numRows, cardWidth, cardHeight});
}
Check out the docs it discusses
Unlike the setState method found in class components, useState does
not automatically merge update objects.
If the new state is computed using the previous state, you can pass a
function to setState. The function will receive the previous value,
and return an updated value. Here’s an example of a counter component
that uses both forms of setState:

Resources