I am new to using React Hooks and was wondering if it was possible to have useEffect() fire only for specific values of state. Basically I have a simple tic tac toe game and I want the function I have to handle the CPU's turn be called after the human player has made their turn, in other words to essentially call handleCPUTurn() after handleClick() completes the new render.
From what I have already tried, I can either get handleCPUTurn to just once, or infinitely until the game ends. below is the code I have relating to handling turns.
const Game = () => {
const [history, setHistory] = useState([Array(9).fill(null)]);
const [stepNumber, setStepNumber] = useState(0);
const [xIsNext, setXIsNext] = useState(true);
const handleCPUTurn = () => {
const pointInHistory = history.slice(0, stepNumber + 1);
const current = pointInHistory[stepNumber];
const squares = [...current];
let randomSqaure = Math.floor(Math.random() * 8);
if (calculateWinner(squares)) return;
while(true){
if (squares[randomSqaure]) {
randomSqaure = Math.floor(Math.random() * 8);
continue;
}
else {
squares[randomSqaure] = 'O';
setHistory([...pointInHistory, squares]);
setStepNumber(pointInHistory.length);
setXIsNext(xIsNext);
break;
}
}
}
const handleClick = (i) => {
const pointInHistory = history.slice(0, stepNumber + 1);
const current = pointInHistory[stepNumber];
const squares = [...current];
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = 'X';
setHistory([...pointInHistory, squares]);
setStepNumber(pointInHistory.length);
setXIsNext(!xIsNext);
}
useEffect(() => (
handleCPUTurn(),[xIsNext]));
You can achieve this by a condition inside useEffect. As I can see you are already passing xIsNext as the dependency in useEffect. So it will call every time when value of xIsNext changes.
What you can do is: PUT A CONDITION
useEffect(() => {
if (!xIsNext) {
handleCPUTurn();
}
} ,[xIsNext]);
This will run handleCPUTurn only when xIsNext=false
Related
I am trying to code a countdown clock and the "mins" is from a brother component.
If I take off setMin from useEffect then it is not changing while the value change in source components, but if I leave it in useEffect it will re-rendering every time if the seconds change and makes it immutable. Anyway, can I fix this problem?
If I need useRef how can I use it with setMin?
export default function Timer(props) {
const { count } = props;
const [isPlay, setIsPlay] = useState(false);
const [isRestore, setIsRestore] = useState(false);
const [second, setSecond] = useState(0);
const [isBreak, setIsBreak] = useState(false);
const [min, setMin] = useState(count)
useEffect(() => {
setMin(count)
let alarm = document.getElementById("beep");
const countdown = () => {
setTimeout(() => {
if (second === 0 && min > 0) {
setMin(min-1);
setSecond(59);
} else if (min >= 0 && second > 0) {
setSecond(second - 1);
} else {
alarm.play();
alarm.addEventListener("ended", () => {
setIsBreak(!isBreak);
});
}
}, 1000);
};
if (isPlay) {
countdown();
} else {
alarm.pause();
}
if (isRestore) {
setIsPlay(false);
alarm.currentTime = 0;
setIsRestore(false);
}
},[isPlay, isRestore, second, isBreak,min,count]);
I think the majority of your logic should be outside of the useEffect hook. Take a look at this answer Countdown timer in React, it accomplishes pretty much the same task as yours and should help you get an idea of the necessary logic
I'm getting the selected checkbox name into an array. Then, I want to use it later for some calculations. but when I try to get the values it's giving the wrong values. I want to allow users to select the ticket count by using the +/- buttons. following is my code
Code
Getting the selected seat from the checkboxes
const [seatCount, setSeatCount] =React.useState("");
const [open, setOpen] = React.useState(false);
const [counter, setCounter] = React.useState(0);
var seatsSelected = []
const handleChecked = async (e) => {
let isChecked = e.target.checked;
if(isChecked){
await seatsSelected.push(e.target.name)
} else {
seatsSelected = seatsSelected.filter((name) => e.target.name !== name);
}
console.log(seatsSelected);
}
calculations happening on a dialog
const handleClickOpen = () => { //open the dialog
setOpen(true);
console.log(seatsSelected);
const seatTotal = seatsSelected.length
console.log(seatTotal)
setSeatCount(seatTotal)
console.log(seatCount)
};
const handleClose = () => { //close the dialog
setOpen(false);
};
const handleIncrement = () =>{ //increse the count
if(counter < seatsSelected.length){
setCounter(counter + 1)
} else {
setCounter(counter)
}
}
const handleDecrement = () =>{ //decrese the count
if(counter >= seatsSelected.length){
setCounter(counter - 1)
} else {
setCounter(counter)
}
}
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, like in your example, the console.log function runs before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
console.log(seatCount);
}, [seatCount);
The callback will run every time the state value changes and only after it has finished changing and a render has occurred.
Basic Situation
Let's say I have a basic useState of a number:
const [valueOne, setValueOne] = useState(0);
I can write an increase function in two ways:
First way:
// for one value
const increaseOneFirstWay = useCallback(() => {
setValueOne((prev) => prev + 1);
}, []); // doesnt have dependency
Since the setter function of a useState doesn't change (source), I don't have to add any dependencies to my callback function.
Second way
const increaseOneSecondWay = useCallback(() => {
setValueOne(valueOne + 1);
}, [valueOne]); // has one dependency
Here, since I am using valueOne, I have to add a dependency, so the callback updates accordingly.
For a basic callback like this, using both ways seems fine. But what if it gets more complicated?
Complicated Situation
Now, instead of having one state, we will have three:
const [valueTwo, setValueTwo] = useState(0);
const [valueThree, setValueThree] = useState(0);
const [valueFour, setValueFour] = useState(0);
This time, the callback will need to use all three values. And some of them together.
First way:
// for several values:
const increaseSeveralFirstWay = useCallback(() => {
setValueTwo((valueTwoPrev) => {
setValueThree((valueThreePrev) => {
setValueFour((valueFourPrev) => {
return valueFourPrev + valueThreePrev + valueTwoPrev + 1;
});
return valueThreePrev + valueTwoPrev + 1;
});
return valueTwoPrev + 1;
});
}, []); // doesnt have dependency
Second way:
const increaseSeveralSecondWay = useCallback(() => {
setValueTwo(valueTwo + 1);
setValueThree(valueThree + valueTwo + 1);
setValueFour(valueFour + valueThree + valueTwo + 1);
}, [valueTwo, valueThree, valueFour]); // has several dependency
Let's say that valueTwo, valueThree, and valueFour also change independently, wouldn't the first way be a better choice? Or is there a reason why someone would use the second way (Not opinion-based, but maybe performance? maybe it's not recommended at all to use the first way?)
Codesandbox
In the case you have multiple states depending on each other the solution is often to use a reducer. However sometimes the use of a reducer is not necessary since the state can be simplified.
I will here demonstrate the 2 solutions with 2 examples:
Solution 1: Using a reducer
useReducer is usually preferable to useState when you have complex
state logic that involves multiple sub-values or when the next state
depends on the previous one. -- React Docs
import { useReducer } from 'react';
const initialNumberState = {
valueOne: 0,
valueTwo: 0,
valueThree: 0,
};
const numberReducer = (prevState, action) => {
if (action.type === 'INCREASE_NUMBER') {
const { valueOne, valueTwo, valueThree } = prevState;
const newValueOne = valueOne + 1;
const newValueTwo = valueOne + valueTwo + 1;
const newValueThree = valueOne + valueTwo + valueThree + 1;
return {
valueOne: newValueOne,
valueTwo: newValueTwo,
valueThree: newValueThree,
};
}
return prevState;
};
const CustomComponent = (props) => {
const [numberState, dispatch] = useReducer(
numberReducer,
initialNumberState
);
const { valueOne, valueTwo, valueThree } = numberState;
const handleClick = () => {
dispatch({ type: 'INCREASE_NUMBER', value: 'not_used_in_this_case' });
};
return (
<div>
<ul>
<li>Number 1: {valueOne}</li>
<li>Number 2: {valueTwo}</li>
<li>Number 3: {valueThree}</li>
</ul>
<button onClick={handleClick}>Click Me!</button>
</div>
);
};
export default CustomComponent;
Solution 2: Simplifying the state
This is the case when we can derive all the data we need from independent states.
For example imagine we are validating a form with separate states:
const [isEmailValid, setIsEmailValid] = useState(false);
const [isPasswordValid, setIsPasswordValid] = useState(false);
const [isFormValid, setIsFormValid] = useState(false);
Here setting the state for the email and password validation is easy. However we start encountering issues when we want to set the state for the form.
handlePasswordChange = (event) =>{
passwordValue = event.currentTarget.value;
const isValid = validatePassword(passwordValue);
setIsPasswordValid(isValid);
const formValid = isPasswordValid && isEmailValid;
setIsFormValid(formValid);
/* Here we will encounter issues since we are updating
the form validity on a stale password validity value; */
}
Here the solution could have been : const formValid = isValid && isEmailValid;
But the optimal solution is simplifying the state:
const [isEmailValid, setIsEmailValid] = useState(false);
const [isPasswordValid, setIsPasswordValid] = useState(false);
const isFormValid = isEmailValid && isPasswordValid;
This is a simplistic example and you might think this never happens. But we often over complicate things.
I have a webapi invoked that is working properly:
const [pItem, setPItem] = useState([]);
const [weight, setWeight] = useReducer(weightHandler, 0.0);
useEffect(() => {
setLoading(true);
let mounted = true;
(async function () {
await getPlantInfoById(itemId)
.then(item => {
if (mounted) {
setPItem(item)
setLoading(false);
}
})
})();
return () => { mounted = false; }
}, [itemId])
Here pItem contains data now I have another filled called weight(which can be changed by a user) .
So I need some calculations according to the weight changes:
const PaymentCalculator = function () {
const [item] = [...pItem];
const priceWithDiscount = DiscountCalc(item.price, item.discount);
const divideWeight = weight / item.weight;
const result = (divideWeight * priceWithDiscount) * 1000;
return result;
}
const use = useMemo(() => PaymentCalculator(), [weight])
But it seems PaymentCalculator invoked before useEffect !!
How can I fix this?
If you examine the contents of paymentCalculator you'll see you've more than just weight as a dependency.
const PaymentCalculator = function () {
const [item] = [...pItem];
const priceWithDiscount = DiscountCalc(item.price, item.discount);
const divideWeight = weight / item.weight;
const result = (divideWeight * priceWithDiscount) * 1000;
return result;
}
pItem is also a dependency!
Initially pItem is an empty array, and since all hooks are called on each render cycle, this would mean that item is undefined on the initial render and accessing item.price and item.discount will throw an error for attempting to "access X of undefined".
Add pItem to the dependency array and provide a fallback value.
const paymentCalculator = function() {
const [item = {}] = [...pItem];
const priceWithDiscount = discountCalc(item.price, item.discount);
const divideWeight = weight / item.weight;
const result = (divideWeight * priceWithDiscount) * 1000;
return result;
}
...
const use = useMemo(() => PaymentCalculator(), [pItem, weight]);
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.