The countdown timer goes to negative - react-hooks

My countdown timer won’t stop after 0 and it went to negative even after I clear the interval. I seem not able to see where it went wrong.
Also after the timer goes to 0, I want the page automatically go to the next page without giving specific route, so I’m thinking using useHistory and goForward() but don’t know where I add the hook in this function. Can I return clearInterval and history.goForward() both?
import React, { useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
const Timer = () => {
const [seconds, setSeconds] = useState(10);
const history = useHistory();
useEffect(() => {
const interval = setInterval(
() => setSeconds((prevTimer) => prevTimer - 1),
1000,
);
if (seconds === 0) {
return () => clearInterval(interval);
}
}, []);
return <div className="countdown">{seconds}</div>;
};
export default Timer;

Your useEffect function is only called once on first render and never again. So you don't actually clear the interval. clearInterval needs to sit outside of that function so that it can be called when the seconds reach zero. I would write your code like so:
export default function App() {
const [seconds, setSeconds] = React.useState(10);
const interval = React.useRef();
React.useEffect(() => {
interval.current = setInterval(
() => setSeconds((prevTimer) => prevTimer - 1),
1000
);
}, []);
if (seconds === 0) {
clearInterval(interval.current);
}
return <div className="countdown">{seconds}</div>;
}
useRef is like a “box” that can hold a mutable value in its .current property.
So here you assign the interval to it and can clear it any time you want.
Sandbox

when you set timer inside useEffect it's not aware of changes. I have updated your code. codepen
export default function App() {
let [seconds, setSeconds] = useState(15),
[timer, setTimer] = useState(null); // IN YOU NEED TO STOP TIMER
useEffect(() => {
if (seconds > 0) updateSeconds();
else {
// go to next page
// set timer
// setSeconds(15);
}
// eslint-disable-next-line
}, [seconds]);
/* Timer Logic */
function updateSeconds() {
let timeOut = setTimeout(() => {
setSeconds(seconds - 1);
}, 1000);
setTimer(timeOut);
}
return (
<div className="App">
<h1>Hello Timer</h1>
<h2>{seconds}</h2>
</div>
);
}

Related

useEffect executes with stale information

If I have a useEffect hook like this:
const [text, setText] = useState("");
useEffect(() => {
async function run() {
// fetch new text when some prop changes
const newText = await fetchText();
// to exaggerate the effect of waiting,
// let's sleep for two seconds
await new Promise((r) => setTimeout(r, 2000));
setText(newText);
}
run();
}, [some_prop]);
Every time some_prop changes, I fetch new text from an endpoint. Suppose run takes 3 seconds to finish. If the value of some_prop changes more often that, useEffect will still resolve all the async calls to run. Suppose some_prop changes from a -> b -> c -> d -> e in one second. I would like to see the value of text go smoothly from a to e, but it will flash through all the intermediate values (b -> c -> d) as the calls to run finish. How can I throw away those intermediate calls?
Add a let variable (cancel) to the useEffect block, and if the useEffect is called, set cancel to true. If cancel is called, avoid setting the state.
Note: The obvious solution in the example is to cancel the timeout, but the timeout simulates an api call.
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
<script src="https://unpkg.com/#babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useState, useEffect } = React;
const Demo = () => {
const [num, setN] = useState(0);
const [text, setText] = useState('Text: ');
useEffect(() => {
let cancel = false;
async function run() {
// simulate api call
await new Promise(r => setTimeout(r, 2000));
if(cancel) return;
setText(`Text: ${num * 10}`);
}
run();
return () => {
cancel = true;
};
}, [num]);
return (
<div>
<button onClick={() => setN(n => n + 1)}>{num}</button>
<div>{text}</div>
</div>
);
}
ReactDOM
.createRoot(root)
.render(<Demo />);
</script>
One caveat is that the api is getting called needlessly, and we don't cancel the requests. We can cancel the actual request, by using fetch or any a library that supports cancelling (axios for example).
If you're using fetch, you just need to pass the signel from an abort controller.
fetch(url, { signal })
Read the docs of other libraries to see how they can be cancelled.
This example uses an abort controller to cancel the timeout.
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
<script src="https://unpkg.com/#babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useState, useEffect } = React;
const Demo = () => {
const [num, setN] = useState(0);
const [text, setText] = useState('Text: ');
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
async function run() {
// simulate api call
await new Promise(r => {
const timeout = setTimeout(r, 2000);
signal.addEventListener('abort', () => {
clearTimeout(timeout);
});
});
setText(`Text: ${num * 10}`);
}
run();
return () => {
controller.abort();
};
}, [num]);
return (
<div>
<button onClick={() => setN(n => n + 1)}>{num}</button>
<div>{text}</div>
</div>
);
}
ReactDOM
.createRoot(root)
.render(<Demo />);
</script>

React Hook / How to pause & continue my timer?

Hello
I am working on a timer in React to understand how Hooks works, and so far everything is ok except the start button (in my case the timer starts automatically and start button should be use with pause). I can't figure how to resolve this problem with these hooks.
const { useRef, useState, useEffect } = React;
function Minuteur() {
const intervalRef = useRef();
const [timer, setTimer] = useState(30);
useEffect(() => {
const id = setInterval(() => {
setTimer((oldTimer) => oldTimer - 1);
}, 1000);
intervalRef.current = id;
}, []);
const stopTimer = () => {
clearInterval(intervalRef.current);
};
const resetTimer = () => {
setTimer(30)
};
const playTimer = () => {
};
return (
<div>
<p>Il reste : {timer} secondes</p>
<button onClick={playTimer}> PLAY! </button>
<button onClick={stopTimer}> STOP! </button>
<button onClick={resetTimer}> RESET! </button>
</div>
);
Codepen
function Minuteur() {
// Définition de la référence
const intervalRef = useRef();
const [timer, setTimer] = useState(30);
const [timerRunning, setTimerRunning] = useState(false); // I added a state for if the timer should be running or not
useEffect(() => {
let interval = null;
if (timerRunning) { // Check if the timer is running
interval = setInterval(() => {
setTimer(timer => timer - 1);
}, 1000);
}
return () => clearInterval(interval);
}, [timerRunning]); // rerun side effect when timerRunning changes
// Fonction permettant d'arrêter le ‘timer’
const stopTimer = () => {
setTimerRunning(false) // Set running to false
};
const resetTimer = () => {
setTimer(30);
stopTimer();
};
const playTimer = () => {
setTimerRunning(true); // set running to true
};
...
}
Edit: Everything in the [] dependency array at the end of the useEffect hook is what the side effect "watches". So by adding the timerRunning to the dependency array the useEffect hook will watch for the timerRunning and when it changes, it will cause the hook to re-render. If it is an empty array then it will only ever run on the initial load. That is why your timer started on refresh.

React - useEffect not using updated value in websocket onmessage

I have a simple issue where a state value updates in my code but is not using the new value. Any ideas what I can do to adjust this?
const [max, setMax] = useState<number>(10);
useEffect(() => {
console.log('max', max); //This outputs correct updated value.
ws.onmessage = (message: string => {
console.log('max', max); //This is always 10.
if (max > 100) {
doSomething(message);
}
}
},[]);
function onChange() {
setMax(1000);
}
<Select onChange={onChange}></Select> //this is abbrev for simplicity
Your useEffect is running only once, on the initial render, using the values from initial render, so max variable is closure captured and not updated in any way. But the solution will be pretty simple, using useRef and one more useEffect to update the ref variable when max variable updates.
const maxRef = useRef(10); // same value
const [max, setMax] = useState(10);
// Only used to update ref variable
useEffect(() => {
maxRef.current = max;
}, [max]);
useEffect(() => {
console.log("max", maxRef.current);
ws.onmessage = (message) => {
console.log("max", maxRef.current);
if (maxRef.current > 100) {
doSomething(message);
}
};
}, []);

How to make a rxjs subscription after a specific action in react hooks?

As we all know that we need to make a subscription in useeffect and unsubscribe it when the component will unmount. But this kind of code will be triggered once the component is mounted. I'm now want to trigger the subscription after a specific action.Look at the code below.
const [timing, setTiming] = useState<number>(60)
const interval$ = interval(1000)
useEffect(() => {
})
const sendCodeOnceSubmit = async (phone: number) => {
const res = await sendCode(phone)
if (res.code !== 200) {
message.error(`${res.message}`)
} else {
interval$.pipe(take(60)).subscribe(() => setTiming(timing - 1))
}
}
I have a form in the dom,and once I click submit,the sendCodeOnceSubmit function will be triggered which will then send a request through sendCode function to the server. Once the server return a success code, I want to make a countdown with rxjs, but how can I unsubscribe it cause the normal way to do it is to subscribe a observable in useeffect. Thanks for anyone who can help.
Just wrap interval$ with useState and write a useEffect for it.
// Moved const out of component.
const defaultTiming = 60;
/* ... */
export default function App() {
const [timing, setTiming] = useState<number>(defaultTiming);
const [interval$, setInterval$] = useState<Observable<number> | undefined>();
useEffect(() => {
if (!interval$) return;
const subscription = interval$.pipe(take(defaultTiming)).subscribe(() => {
setTiming((prev) => prev - 1);
});
return () => subscription.unsubscribe();
}, [interval$]);
const sendCodeOnceSubmit = async (phone: number) => {
const res = await sendCode(phone);
if (res.code !== 200) {
// message.error(`${res.message}`);
console.error(res.message);
} else {
setInterval$(interval(1000));
}
};
return (
<div className="App">
<p>{timing}</p>
<button type="button" onClick={() => sendCodeOnceSubmit(123)}>
Click
</button>
</div>
);
}

Why does my timer hook not update it's internal state?

For some reason my timer is not updating it's internal Timer State after I modify the input field. Here is the intial state of my page and State.
This is what my screen and state look like after I modify the input from 10 to 8 seconds. Notice that the Timer State does not update
Here is my code for the workout page:
function WorkoutPage(props: any) {
const DEFAULT_SECONDS_BETWEEN_REPS: number = 10
const [secondsBetweenRepsSetting, setSecondsBetweenRepsSetting] = useState(DEFAULT_SECONDS_BETWEEN_REPS)
const {secondsLeft, isRunning, start, stop} = useTimer({
duration: secondsBetweenRepsSetting,
onExpire: () => sayRandomExerciseName(),
onTick: () => handleTick(),
})
const onTimeBetweenRepsChange = (event: any) => {
const secondsBetweenRepsSettingString = event.target.value;
const secondsBetweenRepsSettingInt = parseInt(secondsBetweenRepsSettingString)
setSecondsBetweenRepsSetting(secondsBetweenRepsSettingInt)
}
return <React.Fragment>
<input type="number" name="secondsBetweenRepsSetting" value={secondsBetweenRepsSetting} onChange={onTimeBetweenRepsChange}/>
</React.Fragment>
}
Here is my useTimer Class:
import { useState } from 'react';
import Validate from "../utils/Validate";
import useInterval from "./useInterval";
export default function useTimer({ duration: timerDuration, onExpire, onTick}) {
const [secondsLeft, setSecondsLeft] = useState(timerDuration)
const [isRunning, setIsRunning] = useState(false)
function start() {
setIsRunning(true)
}
function stop() {
setIsRunning(false)
}
function handleExpire() {
Validate.onExpire(onExpire) && onExpire();
}
useInterval(() => {
const secondsMinusOne = secondsLeft - 1;
setSecondsLeft(secondsMinusOne)
if(secondsMinusOne <= 0) {
setSecondsLeft(timerDuration) // Reset timer automatically
handleExpire()
} else {
Validate.onTick(onTick) && onTick();
}
}, isRunning ? 1000 : null)
return {secondsLeft, isRunning, start, stop, }
}
My full codebase is here in case someone is interested: https://github.com/kamilski81/bdt-coach
Here's the sequence of events you expect:
User changes the input
The change handler fires and calls setSecondsBetweenRepsSetting with the new value
The component re-renders with the new value for secondsBetweenRepsSetting
useTimer is invoked with a duration property of the new value
The secondsLeft state in the useTimer hook changes to the new duration value <-- oops! this does not happen
Why doesn't this last item happen? Because within the useTimer implementation, the only place you use the duration is as the initial value of secondsLeft. Calling the hook a second time with a new duration value will not change the secondsLeft state, and this is by design.
My recommendation would be to include setSecondsLeft in the return value of the useTimer hook to give you a way to override the time left in the timer. You could then use setSecondsLeft directly in the input change handler:
const { secondsLeft, setSecondsLeft, isRunning, start, stop } = useTimer({
duration: secondsBetweenRepsSetting,
onExpire: () => sayRandomExerciseName(),
onTick: () => handleTick(),
});
const onTimeBetweenRepsChange = (event: any) => {
const secondsBetweenRepsSettingString = event.target.value;
const secondsBetweenRepsSettingInt = parseInt(
secondsBetweenRepsSettingString
);
setSecondsBetweenRepsSetting(secondsBetweenRepsSettingInt);
setSecondsLeft(secondsBetweenRepsSettingInt);
};

Resources