Alert when counter reaches 0 fails - javascript

I try to add a feature to my simple Counter (React) App which alerts when counter reaches 0 onClick increase or decrease button. But alert always late for 1 click. How can I fix it?
Here is my code:
function App() {
const [counter, setCounter] = useState(0);
function handleIncrement() {
setCounter((oldState) => oldState + 1);
if (counter === 0) alert('it is 0');
}
function handleDecrement() {
setCounter((oldState) => oldState - 1);
if (counter === 0) alert('it is 0');
}
return (
<div>
<button onClick={handleIncrement}>increment</button>
<div>{counter}</div>
<button onClick={handleDecrement}>decrement</button>
</div>
);
}
I want to see alert exactly when I see 0 on the screen. But the code above shown alert only after the counter passed zero.

This is happening bcoz setCounter is async operation.
We can fix this using this two ways
wrap alert function inside the setCounter's callback.
function handleIncrement() {
setCounter((oldState) => {
if (oldState + 1 === 0) {
alert('it is');
}
return oldState + 1;
});
}
function handleDecrement() {
setCounter((oldState) => {
if (oldState - 1 === 0) {
alert('it is 0');
}
return oldState - 1;
});
}
you can also use useEffect to achieve this
useEffect(() => {
if (counter === 0) {
alert('it is 0');
}
}, [counter]);
function handleIncrement() {
setCounter(counter + 1);
}
function handleDecrement() {
setCounter(counter - 1);
}

The best way for solving your problem is to handle states update inside the useEffect hook.
Your issue happened because of useState async hook.
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [counter, setCounter] = useState(0);
function handleIncrement() {
setCounter((oldState) => oldState + 1);
}
function handleDecrement() {
setCounter((oldState) => oldState - 1);
}
useEffect(()=>{
if(counter === 0) {
alert('it is')
}
}, [counter])
return (
<div>
<button onClick={handleIncrement}>increment</button>
<div>{counter}</div>
<button onClick={handleDecrement}>decrement</button>
</div>
);
}

that's okay, because react renders in frames like when you watch a movie or play a video game, at the current frame where you call this line
setCounter((oldState) => oldState - 1);
counter still 1 not 0, so you need to wait until react do the re-render, then the next render start with counter being 0, but the alert you defined inside click handler won't be called automatically, you didn't call it, you made that call inside the onClick handler which will not be called until button clicked(obviously)
so the problem is not with how react re-render the state, it's where to put your alert or in other words how to consume your state
a simple fix to your solution is to put alert outside the call, in the component body
function App() {
const [counter, setCounter] = useState(0);
function handleIncrement() {
setCounter((oldState) => oldState + 1);
}
function handleDecrement() {
setCounter((oldState) => oldState - 1);
}
if (counter === 0) alert('it is 0');
return (
<div>
<button onClick={handleIncrement}>increment</button>
<div>{counter}</div>
<button onClick={handleDecrement}>decrement</button>
</div>
);
}
Anyway it's not a good idea to have alert called inside the body of a component so I'd assume you have an alert component that's shown to the user when the counter reach 0
function App() {
const [counter, setCounter] = useState(0);
function handleIncrement() {
setCounter((oldState) => oldState + 1);
}
function handleDecrement() {
setCounter((oldState) => oldState - 1);
}
if(counter == 0){
return <Alert message="counter is 0 now"/>
}
return (
<div>
<button onClick={handleIncrement}>increment</button>
<div>{counter}</div>
<button onClick={handleDecrement}>decrement</button>
</div>
);
}

You can use useEffect, and a ref. The ref stores whether it's the first update or not. The useEffect checks to see if it's the first update. If it is it resets the ref to false, otherwise it logs/alerts the counter state.
(Cribbed slightly from this question.)
const { useEffect, useState, useRef } = React;
function Example() {
const [counter, setCounter] = useState(0);
const firstUpdate = useRef(true);
function handleIncrement() {
setCounter(prev => prev + 1);
}
function handleDecrement() {
setCounter(prev => prev - 1);
}
useEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
if (counter === 0) console.log('it is');
}, [counter]);
return (
<div>
<button onClick={handleIncrement}>increment</button>
<div>{counter}</div>
<button onClick={handleDecrement}>decrement</button>
</div>
);
}
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Related

React setInterval question (using setState inside another setState problem)

I'm new to reactjs; I encountered this problem while studying about useState. I'm trying to decrease the value of the second state when the first state decreases to 0, and the iteration will run until both states are 0. But the second state always decreases by 2, which makes me confused.
This is my code:
import { useState } from "react";
import "./styles.css";
export default function App() {
const [firstCount,setFirstCount]=useState(10);
const [secondCount,setSecondCount] = useState(5);
function decreaseCount(){
const interval= setInterval(()=>{
setFirstCount((prevFirstCount)=>{
if(prevFirstCount>0){
return prevFirstCount-1;
}
else{
setSecondCount((prevSecondCount)=>{
if(prevSecondCount>0){
return prevSecondCount-1
}
else{
clearInterval(interval);
return prevFirstCount
}
})
return 10;
}
})
},1000)
}
return (
<div className="App">
<div>{firstCount}</div>
<div>{secondCount}</div>
<button onClick={(decreaseCount)}>Decrease Count</button>
</div>
);
}
codesandbox link: https://codesandbox.io/s/interval-setcountprob-plpzl?file=/src/App.js:0-835
I'd really appreciate if someone can help me out.
It's because the callback you pass to setFirstCount must be pure, but you violated that contract by trying to use it to mutate secondCount. You can correctly implement this dependency with useRef and useEffect:
export default function App() {
const [firstCount, setFirstCount] = useState(0);
const [secondCount, setSecondCount] = useState(6);
const firstCountRef = useRef(firstCount);
const secondCountRef = useRef(secondCount);
firstCountRef.current = firstCount;
secondCountRef.current = secondCount;
function decreaseCount() {
const interval = setInterval(() => {
if (secondCountRef.current === 0) {
clearInterval(interval);
return;
}
const { current } = firstCountRef;
setFirstCount(prev => (prev + 9) % 10);
setSecondCount(prev => current > 0 ? prev : (prev + 9) % 10);
}, 1000);
}
return (
<div className="App">
<div>{firstCount}</div>
<div>{secondCount}</div>
<button onClick={decreaseCount}>Decrease Count</button>
</div>
);
}
However, it might be easier to use a single state and compute the counts from that:
export default function App() {
const [count, setCount] = useState(60);
const countRef = useRef(count);
const firstCount = count % 10;
const secondCount = Math.floor(count / 10);
countRef.current = count;
function decreaseCount() {
const interval = setInterval(() => {
if (countRef.current === 0) {
clearInterval(interval);
return;
}
setCount(prev => prev - 1);
}, 1000);
}
return (
<div className="App">
<div>{firstCount}</div>
<div>{secondCount}</div>
<button onClick={decreaseCount}>Decrease Count</button>
</div>
);
}
I solved the issue this way :
const [firstCount, setFirstCount] = useState(10);
const [secondCount, setSecondCount] = useState(5);
const handleDecrease = () => {
setInterval(() => {
setFirstCount((prev) => {
if (prev > 0) {
return prev - 1;
}
if (prev === 0) {
return prev + 10;
}
});
}, 1000);
};
React.useEffect(() => {
if (firstCount === 0) {
setSecondCount((prev) => {
if (prev === 0) {
setFirstCount((firstPrev) => firstPrev + 10);
return prev + 5;
} else {
return prev - 1;
}
});
}
}, [firstCount]);
return (
<div className="App">
<div>{firstCount}</div>
<div>{secondCount}</div>
<button onClick={handleDecrease}>Decrease Count</button>
</div>
);
You shouldn't declare functions like this:
function decreaseCount(){
...
Instead you should use useCallback:
const decreaseCount = useCallback(() => {
//your code here
}[firstCount, secondCount]) //dependency array
You should read more about hooks: https://reactjs.org/docs/hooks-intro.html

setTimout with pause/resume counter not updating on render

I would like to setup a counter which can be paused as well as resumed in React.js. But whatever I have tried so far is working the functionality part (pause/resume is working) but it's not updating the counter on render. Below is my code:
const ProgressBar = (props) => {
const [isPlay, setisPlay] = useState(false);
const [progressText, setProgressText] = useState(
props.duration ? props.duration : 20
);
var elapsed,
secondsLeft = 20;
const timer = () => {
// setInterval for every second
var countdown = setInterval(() => {
// if allowed time is used up, clear interval
if (secondsLeft < 0) {
clearInterval(countdown);
return;
}
// if paused, record elapsed time and return
if (isPlay === true) {
elapsed = secondsLeft;
return;
}
// decrement seconds left
secondsLeft--;
console.warn(secondsLeft);
}, 1000);
};
timer();
const stopProgress = () => {
setisPlay(!isPlay);
if (isPlay === false) {
secondsLeft = elapsed;
}
};
return (
<>
<p>{secondsLeft}</p>
</>
);
};
export default ProgressBar;
I have tried React.js state, global var type, global let type, react ref so far to make the variable global but none of them worked..
So basically why does your example not work?
Your secondsLeft variable not connected to your JSX. So each time your component rerendered it creates a new secondsLeft variable with a value of 20 (Because rerendering is simply the execution of your function that returns JSX)
How to make your variable values persist - useState or useReducer hook for react functional component or state for class based one. So react will store all the values for you for the next rerender cycle.
Second issue is React doesn't rerender your component, it just doesn't know when it should. So what causes rerendering of your component -
Props change
State change
Context change
adding/removing your component from the DOM
Maybe I missing some other cases
So example below works fine for me
import { useEffect, useState } from "react";
function App() {
const [pause, setPause] = useState(false);
const [secondsLeft, setSecondsLeft] = useState(20);
const timer = () => {
var countdown = setInterval(() => {
if (secondsLeft <= 0) {
clearInterval(countdown);
return;
}
if (pause === true) {
clearInterval(countdown);
return;
}
setSecondsLeft((sec) => sec - 1);
}, 1000);
return () => {
clearInterval(countdown);
};
};
useEffect(timer, [secondsLeft, pause]);
const pauseTimer = () => {
setPause((pause) => !pause);
};
return (
<div>
<span>Seconds Left</span>
<p>{secondsLeft}</p>
<button onClick={pauseTimer}>{pause ? "Start" : "Pause"}</button>
</div>
);
}
import React, { useState, useEffect } from "react";
import logo from "./logo.svg";
import "./App.css";
var timer = null;
function App() {
const [counter, setCounter] = useState(0);
const [isplayin, setIsPlaying] = useState(false);
const pause = () => {
setIsPlaying(false);
clearInterval(timer);
};
const reset = () => {
setIsPlaying(false);
setCounter(0);
clearInterval(timer);
};
const play = () => {
setIsPlaying(true);
timer = setInterval(() => {
setCounter((prev) => prev + 1);
}, 1000);
};
return (
<div className="App">
<p>Counter</p>
<h1>{counter}</h1>
{isplayin ? (
<>
<button onClick={() => pause()}>Pause</button>
<button onClick={() => reset()}>Reset</button>
</>
) : (
<>
{counter > 0 ? (
<>
<button onClick={() => play()}>Resume</button>
<button onClick={() => reset()}>Reset</button>
</>
) : (
<button onClick={() => play()}>Start</button>
)}
</>
)}
</div>
);
}
export default App;

Why I'm not able to clear my setInterval?

I'm trying to create an automatic counter when the component(Home here for instance) renders in the view. The counter should start at 1 and stop at 10. But I'm not able to clear my setInterval at 10. The counter remains incrementing.
What is the problem?
Thanks a lot.
export const Home = () => {
const [counter, setCounter] = useState(1)
let timer = useRef()
const animate = () => {
if(counter === 10){
clearInterval(timer.current)
}else{
setCounter((previous) => previous + 1)
}
}
useEffect(() => {
timer.current = window.setInterval(() => {
animate()
}, 900)
}, [])
return (
<div style={{textAlign: "center"}}>
<h1>{counter}</h1>
</div>
)
}
Problem is the closure of animate function over the initial value of counter, i.e. 1. As a result, the condition counter === 10 never evaluates to true.
This is why you should never lie about the dependencies of the useEffect hook. Doing so can lead you to bugs because of closures over the stale values from the previous renders of your component.
Solution
You can get rid of animate() function and write the logic inside the useEffect hook.
function App() {
const [counter, setCounter] = React.useState(1);
const counterRef = React.useRef(counter);
React.useEffect(() => {
const id = setInterval(() => {
// if "counter" is 10, stop the interval
if (counterRef.current === 10) {
clearInterval(id);
} else {
// update the "counter" and "counterRef"
setCounter((prevCounter) => {
counterRef.current = ++prevCounter;
return prevCounter;
});
}
}, 900);
// clearn interval on unmount
return () => clearInterval(id);
}, []);
return (
<div style={{ textAlign: "center" }}>
<h1>{counter}</h1>
</div>
);
}
ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I'd create a second useEffect() for clearing the interval:
const { useEffect, useRef, useState } = React;
const Home = () => {
const [counter, setCounter] = useState(1);
const timer = useRef();
useEffect(() => {
timer.current = setInterval(() => {
setCounter(x => x + 1);
}, 900);
// in case the component unmounts before the timer finishes
return () => clearInterval(timer.current);
}, []);
useEffect(() => {
if (counter === 10) {
clearInterval(timer.current);
}
}, [counter]);
return (
<div style={{ textAlign: 'center' }}>
<h1>{counter}</h1>
</div>
);
}
ReactDOM.render(<Home/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
That's because modifying ref.current doesn't trigger a rerender.
You need to use useEffect to clear it.
const animate = () => {
setCounter((previous) => previous + 1);
};
useEffect(() => {
if (counter === 10) clearInterval(timer.current);
}, [counter]);

Re-render text on React

I'm developing a React (Next.js) app that contains a counter regressive (10 seconds), I have this code:
let [time, setTime] = useState(10);
setTime = () => {
setTimeout(() => {
if (time > 0) {
console.log(time);
time -= 1;
setTime();
}
else {
console.log("end");
}
}, 1000);
};
setTime();
Then:
return (
<>
<span>{time}</span>
</>
)
Ok, that works fine on console, print 10, then 9, then 8, and so on until it reaches 0, but the tag <span> keep showing 10 (the initial value).
Thanks, and hope you can help me!
The issue is how you are using the state hook. You are modifying the time value, and you are mutating it inside the setTimeout closure. Please check out how state should be handled: https://reactjs.org/docs/hooks-state.html.
const [time, setTime] = useState(10);
// somewhere else
setTime(time => time - 1)
Use should use setState to update the state
Try this
class App extends React.Component {
state = {
time: 10,
};
setTime = () => {
setTimeout(() => {
if (this.state.time > 0) {
console.log(this.state.time);
this.setState((prev) => ({ time: prev.time - 1 }));
this.setTime();
} else {
console.log("end");
}
}, 1000);
};
render() {
return (
<div className="App">
<span>{this.state.time}</span>
<br />
<br />
<button onClick={this.setTime}>Start counter</button>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
maybe you could try usEffect hook: https://reactjs.org/docs/hooks-effect.html
useful to handle side effects in your component
import { useState, useEffect } from "react";
export default function App() {
let [time, setTime] = useState(10);
useEffect(() => {
setTimeout(() => {
setTime((time) => time - 1);
}, 1000);
});
return <span>{time}</span>;
}
SandBox: https://codesandbox.io/s/dreamy-swanson-heiec?file=/src/App.js

How to create a hook in React that works like this.setState

I need to create a hook that works like this.setState but I don't know how to implement functionality that works like callback in this.setState. Do you have any ideas?
import React, { useState } from 'react';
const useSetState = (initialState) => {
const [state, setState] = useState(initialState)
const setPartialState = (stateToSet, callback) => {
console.log('stateToSet: ', stateToSet);
console.log('callback: ', callback);
if (typeof stateToSet === 'function') {
setPartialState(stateToSet(state));
} else if (typeof state === 'object' &&
typeof stateToSet === 'object' &&
!Array.isArray(state) &&
!Array.isArray(stateToSet)
) {
setState({ ...state, ...stateToSet });
} else {
setState(stateToSet);
}
}
return [state, setPartialState]
}
const App = () => {
const [count, setCount] = useSetState(0);
const checkCount = () => {
if (count === 10) {
console.log('Booooom!');
}
}
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={() => setCount((count + 1, checkCount))}>Add</button>
</div>
);
}
export default App;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
While working with React hooks, you need to use different mindset as compare to when using the class based React components.
If you want to do something when the count is changed to 10, you should do it inside the useEffect hook.
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
if (count === 10) {
console.log('Booooom!');
}
}, [count])
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={() => setCount(count + 1)}>Add</button>
</div>
);
}

Categories

Resources