Why does RxJS interval begin from last value but not from "0"? - reactjs

I need to reset my custom timer to "0 seconds" when I click the Reset button. But when I press Start my timer continues from last value, not from "0 seconds".
const [time, setTime] = useState (0);
const [timerOn, setTimerOn ] = useState (false);
let observable$ = interval(1000);
let subscription = observable$.subscribe(result =>{
if (timerOn) {
setTime(result);
}
});
return () => subscription.unsubscribe();
}, [timerOn]);
return (
<div>
{!timerOn && (
<button onClick={() => setTimerOn(true)}>Start</button>
)}
{ time > 0 && (
<button onClick={() => setTime(0)}>Reset</button>
)}

Problem
Let's take a look at the Reset button:
<button onClick={() => setTime(0)}>Reset</button>
When you click this button it runs the following function:
() => setTime(0)
This just sets the time state back to 0. That's all. It doesn't touch the subscription to the interval observable at all. As a result, the interval subscription will continue emitting numbers in sequence which is why it appears to continue from the last value.
Solution
Your reset function will have to do more than just setting time back to 0. What it does exactly will be up to your specific use case. For example, it could end the subscription, end the subscription and create a new one, or reset the existing subscription. I've included a code example for a basic way of ending the subscription:
const {
StrictMode,
useCallback,
useRef,
useState,
} = React;
const { interval } = rxjs;
const rootElement = document.getElementById("root");
function useCounter() {
const [time, setTime] = useState(0);
const [timerOn, setTimerOn] = useState(false);
const observable$ = interval(1000);
const subscription = useRef();
const start = useCallback(() => {
setTimerOn(true);
subscription.current = observable$.subscribe((result) => {
console.log(result);
setTime(result);
});
});
const stop = useCallback(() => {
setTimerOn(false);
subscription.current.unsubscribe();
});
const reset = useCallback(() => {
setTime(0);
setTimerOn(false);
subscription.current.unsubscribe();
});
return {
time,
timerOn,
start,
stop,
reset
};
}
function Counter() {
const { time, timerOn, start, stop, reset } = useCounter();
return (
<div>
<h1>{time}</h1>
<br />
{!timerOn && <button onClick={() => start()}>Start</button>}
{time > 0 && <button onClick={() => reset()}>Reset</button>}
</div>
);
}
function App() {
return <Counter />;
}
ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
rootElement
);
<div id="root"></div>
<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>
<script src="https://unpkg.com/rxjs#^7/dist/bundles/rxjs.umd.min.js"></script>

Related

Why is clearInterval not stopping my timer?

I am trying to build a timer with three buttons, a start, stop, where it stops on the current integer, and a reset. I have added my code below which results in the following issues.
I thought my stop function would stop the timer from decrementing but it continues to do so. Also, when logging my timer state in the console, you can see it does not update in the console even though it is updating in the DOM. Why is this?
Thank you for any insight at all.
import React from 'react';
import './style.css';
export default function App() {
const [timer, setTimer] = React.useState(50);
const reset = () => {
setTimer(50);
};
const start = () => {
setTimer((prev) => prev - 1);
};
// const interval = setInterval(() => {
// console.log(updated)
// //start() }, 1000)
// }
const interval = () => {
setInterval(() => {
console.log('updated');
console.log(timer);
start();
}, 1000);
};
const stop = () => {
clearInterval(start);
};
return (
<div>
<h1>{timer}</h1>
<button onClick={interval}>start</button>
<button onClick={stop}>stop</button>
<button onClick={reset}>reset</button>
</div>
);
}`
You have a small problem with assigning the actual value for the interval.
Here is how it should be in usage
const interval = setInterval(() => {})
clearInterval(interval)
For your code change, you can create a ref to keep the interval variable and use it to clean up the interval later.
function App() {
const [timer, setTimer] = React.useState(5);
const intervalRef = React.useRef(); //create a ref for interval
const reset = () => {
setTimer(5);
};
const start = () => {
setTimer((prev) => {
if(prev === 0) {
stop();
return 0;
}
return prev - 1;
});
};
// const interval = setInterval(() => {
// console.log(updated)
// //start() }, 1000)
// }
const interval = () => {
//assign interval ref here
intervalRef.current = setInterval(() => {
start();
}, 1000);
};
const stop = () => {
//clear the interval ref
clearInterval(intervalRef.current);
};
return (
<div>
<h1>{timer}</h1>
<button onClick={interval}>start</button>
<button onClick={stop}>stop</button>
<button onClick={reset}>reset</button>
</div>
);
}
ReactDOM.render(
<App/>,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
clearTimeout or clearInterval each take a token, which was returned by a previous call to setTimeout or setInterval. So you'll need to store that token:
const id = setInterval(() => console.log("triggered"), 1000);
// ...
clearInterval(id)
Also, you should be careful about what happens if App is re-rendered, so you should probably put the set/clear logic inside useEffect so you can cleanup your interval.
Also also, although you didn't ask, your console.log(timer) isn't going to work, and will always print 50. The timer variable inside that callback is captured once, and is never updated because that callback is just inside the setInterval now. You'll need to clear and reset your interval with an updated callback function every time App re-renders, or use a ref that you keep updated, which is a pain.
I would recommend borrowing this custom hook that considers all of these things for you: https://usehooks-ts.com/react-hook/use-interval
Then your App component could become extremely simple, but still be robust:
const { useEffect, useRef, useState, useLayoutEffect } = React;
// https://usehooks-ts.com/react-hook/use-interval
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useLayoutEffect(() => {
savedCallback.current = callback;
}, [callback])
useEffect(() => {
if (!delay && delay !== 0) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
function App() {
const [timer, setTimer] = useState(50);
const [running, setRunning] = useState(false);
useInterval(() => setTimer(t => t - 1), running ? 1000 : null);
const start = () => setRunning(true);
const stop = () => setRunning(false);
const reset = () => { setTimer(50); };
return (
<div>
<h1>{timer}</h1><button onClick={start}>start</button>
<button onClick={stop}> stop </button>
<button onClick={reset}> reset </button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("react"));
<div id="react"></div>
<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>
I think you should not wrap your setTimeout into a callable. Then you lose the ability to start and stop it because the variable does not reference the interval but the callable that wraps the interval.
Take a look at this guide: https://www.w3schools.com/jsref/met_win_clearinterval.asp

I want to subtract the value of 'percent' from the function by 0.25 in React

I want to subtract the value of 'percent' from the function by 0.25.
However, subtraction does not work.
I used setState, but I don't know why it doesn't work.
import React, {useState, useRef, useCallback} from 'react';
const Ques = () => {
const [percent,setPercent] = useState(1);
const intervalRef = useRef(null);
const start = useCallback(() =>{
if (intervalRef.current !== null){
return;
}
intervalRef.current = setInterval(()=>{
if (percent > 0){
setPercent(c => c - 0.25);
console.log("percent = ", percent);
}
else {
setPercent(c => 1);
}
}, 1000);
}, []);
return (
<div>
<button onClick={()=>{start()}}>{"Start"}</button>
</div>
);
}
export default Ques;
Issue
The enqueued state updates are working correctly but you've a stale enclosure over the percent state in the interval callback that you are logging, it never will update.
Solution
If you want to log the percent state then use an useEffect hook to log changes.
const Ques = () => {
const [percent, setPercent] = useState(1);
const intervalRef = useRef(null);
useEffect(() => {
console.log("percent = ", percent); // <-- log state changes here
}, [percent]);
const start = useCallback(() => {
if (intervalRef.current !== null) {
return;
}
intervalRef.current = setInterval(() => {
setPercent((c) => Math.max(0, c - 0.25)); // <-- simpler updater function
}, 1000);
}, []);
return (
<div>
Percent: {percent * 100}
<button onClick={start}>Start</button>
</div>
);
};
You can create a ref for percent also and chenge its current value as:
codesandbox link
import React, { useRef, useCallback } from "react";
const Ques = () => {
const percentRef = useRef(1);
const intervalRef = useRef(null);
const start = useCallback(() => {
if (intervalRef.current !== null) {
return;
}
intervalRef.current = setInterval(() => {
console.log("percent = ", percentRef.current);
percentRef.current > 0
? (percentRef.current -= 0.25)
: (percentRef.current = 1);
}, 1000);
}, []);
return (
<div>
<button onClick={start}>Start</button>
</div>
);
};
export default Ques;
I think useCallback and useRef is not a good fit. Below is a minimal verifiable example using useState and useEffect. Note this function appropriately performs cleanup on the timer when the component is unmounted. Click Run to run the code snippet and click start to begin running the effect.
function App() {
const [percent, setPercent] = React.useState(1)
const [running, setRunning] = React.useState(false)
React.useEffect(() => {
if (!running) return
const t = window.setTimeout(() => {
setPercent(c => c > 0 ? c - 0.25 : 1)
}, 1000)
return () => window.clearTimeout(t)
}, [running, percent])
return <div>
<button onClick={() => setRunning(true)} children="start" />
<pre>percent: {percent}</pre>
</div>
}
ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
This may be one possible solution to achieve what is presumed to be the desired objective:
Code Snippet
const {useState, useRef, useCallback} = React;
const Ques = () => {
const [percent,setPercent] = useState(1);
const intervalRef = useRef(null);
const start = useCallback((flag) => {
if (intervalRef.current !== null){
if (flag && flag === 'end') clearInterval(intervalRef.current);
return;
}
intervalRef.current = setInterval(() => {
setPercent(
prev => (prev > 0 ? prev - 0.25 : 1)
);
}, 1000);
}, []);
return (
<div>
percent: {percent} <br/> <br/>
<button onClick={() => start('bgn')}>Start</button>  
<button onClick={() => start('end')}>Stop</button>
</div>
);
}
ReactDOM.render(
<div>
<h3>DEMO</h3>
<Ques />
</div>,
document.getElementById('rd')
);
<div id='rd' />
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
Explanation
There are two buttons Start and Stop
Both invoke the same start method, but with different params (flag)
If intervalRef is already set (ie, not null) and flag is end, clear the interval
The percent is added to the UI to see real-time changes to its value
setPercent is modified to use prev (which holds the correct state)

make a counter in react js and get start stop time and date

can any one make me a counter that starts when i click on start button and stop when i click stop button and also save the clicked time and date in a variable
i want it in functional compontent
<h2>timer </h2>
<div onClick={startcount}>
<button>Start</button>
<br />
<button>Stop</button>
</div>
Live link to test: https://z2pb6.csb.app/
And here is the code:
import { useEffect, useRef, useState } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
const [begin, setBegin] = useState(false);
const [timestamp, setTimestamp] = useState(null);
const intervalRef = useRef(null);
const handleClick = () => setBegin((prev) => !prev);
useEffect(() => {
if (begin) {
setTimestamp(new Date().toString());
// clearing prev interval
intervalRef?.current && clearInterval(intervalRef.current);
const intervalId = setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
intervalRef.current = intervalId;
} else {
setCount(0);
intervalRef?.current && clearInterval(intervalRef.current);
}
}, [begin]);
return (
<div className="App">
<h2>timer {timestamp}</h2>
{count}
<div>
<button onClick={handleClick}>Start</button>
<br />
<button onClick={handleClick}>Stop</button>
</div>
</div>
);
}
You can create a state to store te start and the stop value. like this:
const [timerState, setTimerState] = useState({
start: null,
stop: null
});
When the user click on start button, store the current time
<button onClick={()=>setTimerState({...timerState, start: new Date()})} >Start</button>
When ther user click on stop
<button onClick={()=>setTimerState({...timerState, stop: new Date()})} >Stop</button>

Why my clearInterval in React doesn't work?

when I click to to change code I see inly consols.log. I try to understand it but I can't find the answer..
function App() {
const [laps, setLaps] = useState(0);
const [running, setRunning] = useState(false);
const startTime = Date.now() - laps;
useEffect(() => {
function interval() {
setInterval(() => {
setLaps(Date.now() - startTime);
}, 100);
}
if (!running) {
clearInterval(interval);
console.log('ok');
} else {
interval();
console.log('no');
}
console.log(running);
}, [running]);
return (
<div className="App">
<label>Count: {laps}</label>
<button onClick={() => setRunning(!running)}>{running ? 'stop' : 'start'}</button>
<button>Clear</button>
</div>
);
}
In clean JavaScript this code should works correctly(of course without JSX)?
clearInterval expects a number as argument that is returned from setInterval, but you are giving it the interval function as argument.
You could instead just create the interval if running is true, and return a cleanup function from useEffect that will be run the next time the effect is run.
const { useState, useEffect } = React;
function App() {
const [laps, setLaps] = useState(0);
const [running, setRunning] = useState(false);
const startTime = Date.now() - laps;
useEffect(
() => {
if (running) {
const interval = setInterval(() => {
setLaps(Date.now() - startTime);
}, 100);
return () => clearInterval(interval);
}
},
[running]
);
return (
<div className="App">
<label>Count: {laps}</label>
<button onClick={() => setRunning(!running)}>
{running ? "stop" : "start"}
</button>
<button>Clear</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>

how do I clearInterval on-click, with React Hooks?

I'm trying to refactor my code to react hooks, but I'm not sure if i'm doing it correctly. I tried copying and pasting my setInterval/setTimout code into hooks, but it did not work as intended. After trying different things I was able to get it to work, but I'm not sure if this is the best way to do it.
I know i can use useEffect to clear interval on un-mount, but I want to clear it before un-mounting.
Is the following good practice and if not what is a better way of clearing setInterval/setTimout before un-mounting?
Thanks,
useTimeout
import { useState, useEffect } from 'react';
let timer = null;
const useTimeout = () => {
const [count, setCount] = useState(0);
const [timerOn, setTimerOn] = useState(false);
useEffect(() => {
if (timerOn) {
console.log("timerOn ", timerOn);
timer = setInterval(() => {
setCount((prev) => prev + 1)
}, 1000);
} else {
console.log("timerOn ", timerOn);
clearInterval(timer);
setCount(0);
}
return () => {
clearInterval(timer);
}
}, [timerOn])
return [count, setCount, setTimerOn];
}
export default useTimeout;
Component
import React from 'react';
import useTimeout from './useTimeout';
const UseStateExample = () => {
const [count, setCount, setTimerOn] = useTimeout()
return (
<div>
<h2>Notes:</h2>
<p>New function are created on each render</p>
<br />
<h2>count = {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>Increment</button>
<br />
<button onClick={() => setCount(prev => prev - 1)}>Decrement</button>
<br />
<button onClick={() => setTimerOn(true)}>Set Interval</button>
<br />
<button onClick={() => setTimerOn(false)}>Stop Interval</button>
<br />
</div>
);
}
export default UseStateExample;
--- added # 2019-02-11 15:58 ---
A good pattern to use setInterval with Hooks API:
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
--- origin answer ---
Some issues:
Do not use non-constant variables in the global scope of any modules. If you use two instances of this module in one page, they’ll share those global variables.
There’s no need to clear timer in the “else” branch because if the timerOn change from true to false, the return function will be executed.
A better way in my thoughts:
import { useState, useEffect } from 'react';
export default (handler, interval) => {
const [intervalId, setIntervalId] = useState();
useEffect(() => {
const id = setInterval(handler, interval);
setIntervalId(id);
return () => clearInterval(id);
}, []);
return () => clearInterval(intervalId);
};
Running example here:
https://codesandbox.io/embed/52o442wq8l?codemirror=1
In this example, we add a couple of things...
A on/off switch for the timeout (the 'running' arg) which will completely switch it on or off
A reset function, allowing us to set the timeout back to 0 at any time:
If called while it's running, it'll keep running but return to 0.
If called while it's not running, it'll start it.
const useTimeout = (callback, delay, running = true) => {
// save id in a ref so we make sure we're always clearing the latest timeout
const timeoutId = useRef('');
// save callback as a ref so we can update the timeout callback without resetting it
const savedCallback = useRef();
useEffect(
() => {
savedCallback.current = callback;
},
[callback],
);
// clear the timeout and start a new one, updating the timeoutId ref
const reset = useCallback(
() => {
clearTimeout(timeoutId.current);
const id = setTimeout(savedCallback.current, delay);
timeoutId.current = id;
},
[delay],
);
// keep the timeout dynamic by resetting it whenever its' deps change
useEffect(
() => {
if (running && delay !== null) {
reset();
return () => clearTimeout(timeoutId.current);
}
},
[delay, running, reset],
);
return { reset };
};
So in your example above, we could use it like so...
const UseStateExample = ({delay}) => {
// count logic
const initCount = 0
const [count, setCount] = useState(initCount)
const incrementCount = () => setCount(prev => prev + 1)
const decrementCount = () => setCount(prev => prev - 1)
const resetCount = () => setCount(initCount)
// timer logic
const [timerOn, setTimerOn] = useState(false)
const {reset} = useTimeout(incrementCount, delay, timerOn)
const startTimer = () => setTimerOn(true)
const stopTimer = () => setTimerOn(false)
return (
<div>
<h2>Notes:</h2>
<p>New function are created on each render</p>
<br />
<h2>count = {count}</h2>
<button onClick={incrementCount}>Increment</button>
<br />
<button onClick={decrementCount}>Decrement</button>
<br />
<button onClick={startTimer}>Set Interval</button>
<br />
<button onClick={stopTimer}>Stop Interval</button>
<br />
<button onClick={reset}>Start Interval Again</button>
<br />
</div>
);
}
Demo of clear many timers.
You should declare and clear timer.current instead of timer.
Declare s and timer.
const [s, setS] = useState(0);
let timer = useRef<NodeJS.Timer>();
Initialize timer in useEffect(() => {}).
useEffect(() => {
if (s == props.time) {
clearInterval(timer.current);
}
return () => {};
}, [s]);
Clear timer.
useEffect(() => {
if (s == props.time) {
clearInterval(timer.current);
}
return () => {};
}, [s]);
After many attempts to make a timer work with setInterval, I decided to use setTimeOut, I hope it works for you.
const [count, setCount] = useState(60);
useEffect(() => {
if (count > 0) {
setTimeout(() => {
setCount(count - 1);
}, 1000);
}
}, [count]);

Resources