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:
Related
I have a React table component where both Column component and a button component rendered inside a column, triggers some function. Example of the table:
<Table>
<Column onCellClick={handleCell}>
<button onClick={handleButton} />
</Column>
</Table>
Those 2 handle functions are called at the same click, and they trigger some useStates:
const [cell, setCell] = useState(false);
const [buttonValue, setButtonValue] = useState(false);
const handleCell = (value) => setCell(value)
const handleButton = (value) => setButtonValue(value)
SO I have a useEffect that must trigger some code ONLY when both cell and buttonValue are updates. Currently I have something like:
useEffect(() => {
if (cell && buttonValue) {
// some code
setAnotherState(etc)
}
}, [cell, buttonValue]);
My problem is if I click very quickly or in random scenarios, the setAnotherState is called before the buttonValue or cell are actually updated. With that if inside the useEffect, it will work correctly only the very first time that I actually update both values, because they both are initialized as false, but then, with many clicks, there are some outdated set states.
Any hint? how could I ensure that both states actually update before executing the code inside the if?
I can't remove any of that 2 onClick or onCellClick, they both have different and specific values for their components, so I need them both.
Here's an idea using useRef to store the collective state as an integer value you can test against.
Cell adds 2 to the state (first taking a modulus of 2 to remove existing 2 values), Button adds 1 (first doing a bit-shift and x2 to eliminate the first bit). When the stateRef is 3, the effect knows both have been changed, sets the other state, and resets the stateRef to 0.
const [cell, setCell] = useState(false);
const [buttonValue, setButtonValue] = useState(false);
const stateRef = useRef(0);
const handleCell = (value) => {
setCell(value);
stateRef.current = stateRef.current % 2 + 2;
};
const handleButton = (value) => {
setButtonValue(value);
stateRef.current = (stateRef.current >> 1) * 2 + 1;
};
Then
useEffect(() => {
if (stateRef.current == 3) {
// some code
setAnotherState(etc)
stateRef.current = 0;
}
}, [stateRef.current]);
You could do something like this:
const [cell, setCell] = useState(null);
// it stores the updated status for cell
const [cellStatus, setCellStatus] = useState(false);
const [buttonValue, setButtonValue] = useState(null);
// it stores the updated status for buttonValue
const [buttonValueStatus, setButtonValueStatus] = useState(false);
const handleCell = (value) => {
setCell(value)
setCellStatus(true) // it carried a updated event
}
const handleButton = (value) => {
setButtonValue(value)
setButtonValueStatus(true) // it carried a updated event
}
useEffect(() => {
// !!(null) === false
if (!!cellStatus && !!buttonValueStatus) {
// some code
setAnotherState(etc)
// here you reset their status since they were already updated and used
// now they are not new values but old ones
setCellStatus(false)
setButtonValueStatus(false)
}
}, [cell, buttonValue]);
Thus, the basic idea is to keep track of the values in one variable and the update status in another one.
I'm trying to create a grid of Nodes, which update when clicking/dragging on them. I'm running into a weird React State where the state is updating after the code following it runs.
Expected behaviour:
nodeTypePointer is set to 0 as default react state
Click on Blank Node, nodeTypePointer is set to 0 (unchanged) + logic uses 0
Click on Coloured Node, nodeTypePointer is set to 1 + logic uses 1
Actual behaviour
nodeTypePointer is set to 0 as default react state
Click on Blank Node, nodeTypePointer remains unchanged + logic uses 0
Click on Coloured Node for the first time, nodeTypePointer is set to 0 (unchanged) + logic uses 0, then after code is finished nodeTypePointer is set to 1
Click on Coloured Node for the second time, nodeTypePointer is now still set to 1, logic uses 1, then after code is finished nodeTypePointer is set to 0
I know that the state is async but unsure about the exact reason/problem/solution to go on. Unsure about how to pass parameters to the next functions if I were to use a UseEffect.
Here is the code:
import React, { useEffect, useState } from 'react';
import Node from './Node'
import './Grid.css';
const rectDiameter = 25
const gridHeightRatio = 0.9
const Grid: React.FC = () => {
const gridHeight: number = Math.floor(window.innerHeight * gridHeightRatio)
const gridWidth: number = window.innerWidth
const numX: number = Math.floor(gridHeight / rectDiameter)
const numY: number = Math.floor(gridWidth / rectDiameter)
const tempGrid: number[][] = [...Array(numX)].map(() => Array(numY).fill(0));
const [grid, setGrid] = useState(tempGrid)
const [isMousePressed, setMousePressed] = useState(false)
const [nodeTypePointer, setNodeTypePointer] = useState(0)
useEffect(() => {
console.log('Use Effect', nodeTypePointer)
}, [nodeTypePointer])
// Hacky el.ID to workaround React performance until I know how to do it better
const paintNode = (x: number, y: number) => {
const el = document.getElementById(`${x}-${y}`)
if (!el) return
if (nodeTypePointer === 0) {
el.classList.add("node-wall")
} else {
el.classList.remove("node-wall")
}
}
const updateGridPosition = (x: number, y: number) => {
const newValue = nodeTypePointer === 0 ? 1 : 0
let newGrid: number[][] = grid
newGrid[x][y] = newValue
setGrid(newGrid)
}
const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>, x: number, y: number) => {
console.log('Node pointer type Before', nodeTypePointer)
setNodeTypePointer(grid[x][y])
console.log('Node pointer type After', nodeTypePointer)
setMousePressed(true)
console.log('Before updated Grid', grid[x][y])
updateGridPosition(x, y)
console.log('After updated Grid', grid[x][y])
paintNode(x, y) // <------------- Uses nodeTypePointer
}
const handleMouseUp = (event: React.MouseEvent<HTMLDivElement>, x: number, y: number) => {}
const handleHover = (event: React.MouseEvent<HTMLDivElement>, x: number, y: number) => {
if (!isMousePressed) return
updateGridPosition(x, y)
paintNode(x, y)
}
return (
<div className='gridContainer' >
{
grid.map((row: number[], i: number) =>
row.map((val: number, j: number) =>
<Node
x={i}
y={j}
d={rectDiameter}
key={`${i}-${j}`}
state={val}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onHover={handleHover} />
)
)
}
</div>
);
}
export default Grid
Any help on understanding the problem and suggested solutions would be greatly appreciated!
State changes are asynchronous (both component ones and hook ones). So your state hasn't update by the time you call paintNode. There's 2 ways I can think of around it. One is to put the call to paintNode in your nodeTypePointer's useEffect, like so:
useEffect(() => {
paintNode(x,y);
}, [nodeTypePointer])
but this means you'll need to store the x and y of clicked one in some standard variables, since you won't be able to pass them directly to useEffect. Another way (and maybe a better one, since I'm not a huge fan of state changes having side effects) is to just pass the x/y directly into paintNode during the handleMouseDown function:
paintNode(x, y, grid[x][y])
however it looks like updateGridPosition also needs the x/y, so maybe you would be better off storing the raw clicked one in state, then having a useEffect for the clicked one that does everything needed once a node is clicked. All depends on how this function is gonna end up looking.
But yeah, the reason for your bug is cause state changes are async so it hasn't updated by the time you call paintNode
I am rendering photos from unsplash api. And I am keeping the index of the photos to be used in the lightbox, after the initial render state of imageindex goes back to 0, how can I retain its value?
I will show some code
const ImageList = ({ image, isLoaded }) => {
const [imageIndex, setImageIndex] = useState(0);
const [isOpen, setIsOpen] = useState('false');
const onClickHandler = (e) => {
setIsOpen(true);
setImageIndex(e.target.id);
};
const imgs = image.map((img, index) => (
<img
id={index}
key={img.id}
src={img.urls.small}
onClick={onClickHandler}
if (isOpen === true) {
return (
<Lightbox
onCloseRequest={() => setIsOpen(false)}
mainSrc={image[imageIndex].urls.regular}
onMoveNextRequest={() => setImageIndex((imageIndex + 1) % image.length)}
onMovePrevRequest={() => setImageIndex((imageIndex + image.length - 1) % image.length)}
nextSrc={image[(imageIndex + 1) % image.length].urls.regular}
prevSrc={image[(imageIndex + image.length - 1) % image.length].urls.regular}
/>
after the initial render state, imageIndex goes back to 0.
That makes sense, the initial render would use whatever you set as the default value. You can use something like local storage to help you keep track of the index of the last used item. It's a bit primitive, but until you integrate something like Node/MongoDB for database collections, this will be perfect.
In your component, import useEffect() from React. This hook lets us execute some logic any time the state-index value changes, or anything else you might have in mind.
import React, { useEffect } from "react"
Then inside your component, define two useEffect() blocks.
Getting last used index from localStorage on intitial load:
useEffect(() => {
const lastIndex = localStorage.getItem("index", imageIndex)
setImageIndex(imageIndex)
}, []) //set as an empty array so it will only execute once.
Saving index to localStorage on change:
useEffect(() => {
localStorage.setItem("index", imageIndex)
}, [imageIndex]) //define values to subscribe to here. Will execute anytime value changes.
Considering the following declaration:
const [stateObject, setObjectState] = useState({
firstKey: '',
secondKey: '',
});
Are the following snippets both corrects ?
A)
setObjectState((prevState) => ({
...prevState,
secondKey: 'value',
}));
B)
setObjectState({
...stateObject,
secondKey: 'value',
}));
I am sure that A) is correct, but is it necessary ? B) seems ok, but as setObjectState is an asynchronous function, stateObject might not have the most recent value.
One useful thing about case of A that I have found is that you can use this method to update state from child components while only passing down a single prop for setObjectState. For example, say you have parent component with state you would like to update from the child component.
Parent Component:
import React, {useState} from 'react';
import ChildComponent from './ChildComponent';
export const ParentComponent = () => {
const [parentState, setParentState] = useState({
otherValue: null,
pressed: false,
});
return (
<ChildComponent setParentState={setParentState} />
)
}
Child Component:
import React from 'react';
export const ChildComponent = (props) => {
const callback = () => {
props.setParentState((prevState) => ({
...prevState
pressed: true,
}))
}
return (
<button onClick={callback}>test button<button>
)
}
When the button is pressed, you should expect to see that the state has been updated while also keeping its initial values. As for the difference between the two, there isn't much as they both accomplish the same thing.
A will always give you the updated value. B could be correct but might not. Let me give an example:
const Example = props => {
const [counter, setCounter] = useState(0);
useEffect(() => {
// 0 + 1
// In this first case the passed value would be the same as using the callback.
// This is because in this cycle nothing has updated counter before this point.
setCounter(counter + 1);
// 1 + 1
// Thanks to the callback we can get the current value
// which after the previous iexample is 1.
setCounter(latest_value => latest_value + 1);
// 0 + 1
// In this case the value will be undesired as it is using the initial
// counter value which was 0.
setCounter(counter + 1);
}, []);
return null;
};
When the new value depends on the updated one use the callback, otherwise you can simply pass the new value.
const Example = props => {
const [hero, setHero] = useState('Spiderman');
useEffect(() => {
// Fine to set the value directly as
// the new value does not depend on the previous one.
setHero('Batman');
// Using the callback here is not necessary.
setHero(previous_hero => 'Superman');
}, []);
return null;
};
Also in the example you are giving it would probably be better to use two different states:
const [firstKey, setFirstKey] = useState("");
const [secondKey, setSecondKey] = useState("");
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