I update a useRef.current value in an useEffect, and I want to use that value in another useEffect. But then I find that it doesn't get the updated value, even though I console it in global and make sure it do updated.
const articleAmountRef = useRef<number>(0);
useEffect(() => {
if (windowResized === "small" || windowResized === undefined) return;
let isFetching = false;
let isPaging = true;
let paging = 0;
const el = scrollRef.current;
setArticles([]);
async function queryNews(input: string) {
isFetching = true;
setIsLoading(true);
setScrolling(false);
setSearchState(true);
setPageOnLoad(true);
const resp = await index.search(`${input}`, {
page: paging,
});
const hits = resp?.hits as ArticleType[];
/////////Change useRef value here///////////
articleAmountRef.current = resp?.nbHits;
setArticles((prev) => [...prev, ...hits]);
setIsLoading(false);
paging = paging + 1;
if (paging === resp?.nbPages) {
isPaging = false;
setScrolling(true);
return;
}
isFetching = false;
setScrolling(true);
setSearchState(false);
setPageOnLoad(false);
}
async function scrollHandler(e: WheelEvent) {
if (el!.scrollWidth - (window.innerWidth + el!.scrollLeft) <= 200) {
if (e.deltaY < 0 || isFetching || !isPaging) return;
queryNews(keyword);
}
}
queryNews(keyword);
el?.addEventListener("wheel", scrollHandler);
return () => {
el?.removeEventListener("wheel", scrollHandler);
};
}, [keyword, setArticles, setSearchState, windowResized]);
console.log(
"outside useEffect,articleAmountRef.current=",
articleAmountRef.current
);
/////////use updated useRef value in this useEffect///////////
useEffect(() => {
if (windowResized === "small" || windowResized === undefined) return;
blockWidth.current = newsBlockRef.current?.offsetWidth!;
console.log(
"inside useEffect,articleAmountRef.current=",
articleAmountRef.current
);
if (windowWidth >= 1280) {
contentLength.current =
Math.ceil(articleAmountRef.current / 2) * blockWidth.current +
Math.ceil(articleAmountRef.current / 2) * 60;
}
if (windowWidth < 1280) {
contentLength.current =
Math.ceil(articleAmountRef.current / 2) * blockWidth.current +
Math.ceil(articleAmountRef.current / 2) * 30;
}
}, [windowResized]);
console.log results looks like below:
useRef result
How can I make second useRef get updated value?I want second useRef has value "11476". I tried to write articleAmountRef.current as second useRef's dependency, but eslint warning me "Mutable values like 'articleAmountRef.current' aren't valid dependencies because mutating them doesn't re-render the component."
Since value "11476" is a result of fetch data, I want it update after fetch data getting back. And this value only needs to fetch once, it doesn't need to update every time component re-render, so I tried to save this value by useRef instead of uesState.
Thank you for your kindly help!
I'm making a rock, paper, scissors game.
I want to increment wins after player has selected their option.
I get an error as if the if statement is unaware of any value.
Heres the code:
What the state looks like
const [wins, setWins] = useState(0);
const [playerSelect, setPlayerSelect] = useState(null);
const [computerSelect, setComputerSelect] = useState(null);
// in browser
{id: 1, name:'rock'}
Player select function
const handleSelection = choice => {
const optionClicked = options.find(o => o.id === choice);
setPlayerSelect(optionClicked);
if (playerSelect.name === 'rock' && computerSelect.name === 'scissors') {
setWins(wins => wins + 1);
}
};
I have tried added precautions to check that playerSelect state contains a value first to no prevail. win state in not incremented.
Example:
const handleSelection = choice => {
const optionClicked = options.find(o => o.id === choice);
setPlayerSelect(optionClicked);
if (
playerSelect &&
playerSelect.name &&
playerSelect.name === 'rock' &&
computerSelect.name === 'scissors'
) {
setWins(wins => wins + 1);
}
};
Edit: I have tried the following code and it works after the second click only.
const checkWhoWins = () => {
if (playerSelect.name === 'rock' && computerSelect.name === 'scissors') {
setWins(wins => wins + 1);
}
};
playerSelect && checkWhoWins();
if you want to do an action after a state update you would use useEffect for that, and specify its dependencies as second argument:
useEffect(() => {
// you can use optional chaining to avoid triggering errors here
if (playerSelect?.name === 'rock' && computerSelect?.name === 'scissors') {
setWins(wins => wins + 1);
}
}, [playerSelect, computerSelect])
// playerSelect & computerSelect are the correct dependencies here
I am using primereact's Autocomplete component. The challenge is that I don't want to set the options array to the state when the component loads; but instead I fire an api call when the user has typed in the first 3 letters, and then set the response as the options array (This is because otherwise the array can be large, and I dont want to bloat the state memory).
const OriginAutocomplete = () => {
const [origins, setOrigins] = useState([]);
const [selectedOrigin, setSelectedOrigin] = useState(null);
const [filteredOrigins, setFilteredOrigins] = useState([]);
useEffect(() => {
if (!selectedOrigin || selectedOrigin.length < 3) {
setOrigins([]);
}
if (selectedOrigin && selectedOrigin.length === 3) {
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
});
}
}, [selectedOrigin, setOrigins]);
const handleSelect = (e) => {
//update store
}
const searchOrigin = (e) => {
//filter logic based on e.query
}
return (
<>
<AutoComplete
value={selectedOrigin}
suggestions={ filteredOrigins }
completeMethod={searchOrigin}
field='code'
onChange={(e) => { setSelectedOrigin(e.value) }}
onSelect={(e) => { handleSelect(e) }}
className={'form-control'}
placeholder={'Origin'}
/>
</>
)
}
Now the problem is that the call is triggered when I type in 3 letters, but the options is listed only when I type in the 4th letter.
That would have been okay, infact I tried changing the code to fire the call when I type 2 letters; but then this works as expected only when I key in the 3rd letter after the api call has completed, ie., I type 2 letters, wait for the call to complete and then key in the 3rd letter.
How do I make the options to be displayed when the options array has changed?
I tried setting the filteredOrigins on callback
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
setFilteredOrigins([...origins])
});
But it apparently doesn't seem to work.
Figured it out. Posting the answer in case someone ponders upon the same issue.
I moved the code inside useEffect into the searchOrigin function.
SO the searchOrigin functions goes like below:
const searchOrigin = (e) => {
const selectedOrigin = e.query;
if (!selectedOrigin || selectedOrigin.length === 2) {
setOrigins([]);
setFilteredOrigins([]);
}
if (selectedOrigin && selectedOrigin.length === 3) {
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
setFilteredOrigins(origins);
});
}
if (selectedOrigin && selectedOrigin.length > 3) {
const filteredOrigins = (origins && origins.length) ? origins.filter((origin) => {
return origin.code
.toLowerCase()
.startsWith(e.query.toLowerCase()) ||
origin.name
.toLowerCase()
.startsWith(e.query.toLowerCase()) ||
origin.city
.toLowerCase()
.startsWith(e.query.toLowerCase())
}) : [];
setFilteredOrigins(filteredOrigins);
}
}
The state doesn't update the value even though I'm setting it to the oldvalue + 1.
When logging out the values of ltrNewValue or rtlNewValue it's always the same. It's as it's being overwritten by the initial state.
const Row = (props) => {
const [rowState, setRowState] = useState({
renderInterval: null,
value: 0,
});
useEffect(() => {
const interval = setInterval(counterIntervalFunction, props.speed);
setRowState({ ...rowState, renderInterval: interval });
}, []);
const counterIntervalFunction = () => {
if (props.isRunning && props.direction === 'ltr') {
const ltrNewValue = rowState.value === 2 ? 0 : rowState.value + 1;
console.log(ltrNewValue); // always 1
setRowState({ ...rowState, value: ltrNewValue });
console.log(rowState.value); // always 0
props.setRotatingValue(props.index, rowState.value);
} else if (props.isRunning && props.direction === 'rtl') {
const rtlNewValue = rowState.value === 0 ? 2 : rowState.value - 1;
setRowState({ ...rowState, value: rtlNewValue });
props.setRotatingValue(props.index, rowState.value);
} else {
clearCounterInterval();
}
};
My end goal is to increment the rowState.value up to 2 and then setting it to 0 in a infinite loop. How do I do this correctly?
I'm not certain, but it looks like you have a problem with a stale callback here.
useEffect(() => {
const interval = setInterval(counterIntervalFunction, props.speed);
setRowState({ ...rowState, renderInterval: interval });
}, []);
This effect only runs once - When the component is mounted the first time. It uses the counterIntervalFunction function for the interval:
const counterIntervalFunction = () => {
if (props.isRunning && props.direction === 'ltr') {
const ltrNewValue = rowState.value === 2 ? 0 : rowState.value + 1;
console.log(ltrNewValue); // always 1
setRowState({ ...rowState, value: ltrNewValue });
console.log(rowState.value); // always 0
props.setRotatingValue(props.index, rowState.value);
} else if (props.isRunning && props.direction === 'rtl') {
const rtlNewValue = rowState.value === 0 ? 2 : rowState.value - 1;
setRowState({ ...rowState, value: rtlNewValue });
props.setRotatingValue(props.index, rowState.value);
} else {
clearCounterInterval();
}
};
The counterIntervalFunction captures the reference of props and uses it to determine what to display to the user. However, because this function is only run when the component is mounted, the event will only be run with the props passed to the function originally! You can see an example of this happening in this codesandbox.io
This is why you should put all external dependencies inside of the dependencies array:
useEffect(() => {
const interval = setInterval(counterIntervalFunction, props.speed);
setRowState({ ...rowState, renderInterval: interval });
}, [counterIntervalFunction, props.speed, rowState]);
However, this will cause an infinite loop.
Setting state in useEffect is usually considered a bad idea, because it tends to lead to infinite loops - changing the state will cause the component to re-render, causing another effect to be triggered etc.
Looking at your effect loop, what you're actually interested in is capturing a reference to the interval. This interval won't actually have any impact on the component if it changes, so instead of using state, we can use a ref to keep track of it. Refs don't cause re-renders. This also means we can change value to be a stand-alone value.
Because we now no longer depend on rowState, we can remove that from the dependencies array, preventing an infinite render. Now our effect only depends on props.speed and counterIntervalFunction:
const renderInterval = React.useRef();
const [value, setValue] = React.useState(0);
useEffect(() => {
renderInterval.current = setInterval(counterIntervalFunction, props.speed);
return () => {
cancelInterval(renderInterval.current);
};
}, [props.speed, counterIntervalFunction]);
This will work, but because counterIntervalFunction is defined inline, it will be recreated every render, causing the effect to trigger every render. We can stablize it with React.useCallback(). We'll also want to add all the dependencies of this function to ensure that we don't capture stale references to props and we can change setRowState to setValue. Finally, because the interval is cancelled by useEffect, we don't need to call clearCounterInterval anymore.
const counterIntervalFunction = React.useCallback(() => {
if (props.isRunning && props.direction === 'ltr') {
const ltrNewValue = value === 2 ? 0 : value + 1;
setValue(ltrNewValue);
props.setRotatingValue(props.index, ltrNewValue);
} else if (isRunning && props.direction === 'rtl') {
const rtlNewValue = value === 0 ? 2 : value - 1;
setValue(rtlNewValue);
props.setRotatingValue(props.index, rtlNewValue);
}
}, [value, props]);
This can be simplified even further by moving the required props to the arguments:
const counterIntervalFunction = React.useCallback((isRunning, direction, setRotatingValue, index) => {
if (isRunning === false) {
return;
}
if (direction === 'ltr') {
const ltrNewValue = value === 2 ? 0 : value + 1;
setValue(ltrNewValue);
setRotatingValue(index, ltrNewValue);
} else if (props.direction === 'rtl') {
const rtlNewValue = value === 0 ? 2 : value - 1;
setValue(rtlNewValue);
setRotatingValue(index, rtlNewValue);
}
}, [value]);
This could be even simpler if not for setRotatingValue: Right now, you have a component that both maintains it's own state and tells the parent when its state changes. You should be aware that the component state value might not necessarily update when you call it, but setRotatingValue absolutely will. This may lead to a situation where the parent sees a different state than the child does. I would recommend altering the way your data flows such that it's the parent that owns the current value and passes it via props, not the child.
This gives us the following code to finish off:
function Row = (props) => {
const renderInterval = React.useRef();
const [value, setValue] = React.useState(0);
useEffect(() => {
renderInterval.current = setInterval(counterIntervalFunction, props.isRunning, props.direction, props.setRotatingValue, props.index);
return () => {
cancelInterval(renderInterval.current);
};
}, [props, counterIntervalFunction]);
const counterIntervalFunction = React.useCallback((isRunning, direction, setRotatingValue, index) => {
if (isRunning === false) {
return;
}
if (direction === 'ltr') {
const ltrNewValue = value === 2 ? 0 : value + 1;
setValue(ltrNewValue);
setRotatingValue(index, ltrNewValue);
} else if (props.direction === 'rtl') {
const rtlNewValue = value === 0 ? 2 : value - 1;
setValue(rtlNewValue);
setRotatingValue(index, rtlNewValue);
}
}, [value]);
...
}
In this code, you'll notice that we run the effect every time the props or the function changes. This will mean that, unfortunately, the effect will return every loop, because we need to keep a fresh reference to value. This component will always have this problem unless you refactor counterIntervalFunction to not notify the parent with setRotatingValue or for this function to not contain its own state. An alternatively way we could solve this would be using the function form of setValue:
const counterIntervalFunction = React.useCallback((isRunning, direction, setRotatingValue, index) => {
if (isRunning === false) {
return;
}
setValue(value => {
if (direction === 'ltr') {
return value === 2 ? 0 : value + 1;
} else if (direction ==' rtl') {
return value === 0 ? 2 : value - 1;
}
})
}, []);
Because the state update is not guaranteed to run synchronously, there's no way to extract the value from the setValue call and then call the setRotatingValue function, though. :( You could potentially call setRotatingValue inside of the setValue callback but that gives me the heebie geebies.
It's an interval and it may mess things up when you call setState directly by relying on the old state by the name rowState, try this:
setRowState(oldstate=> { ...rowState, value: oldstate.value+1 });
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.