I'm following a beginner's tutorial for the useState and useRef hooks, trying to implement a simple timer in react.
I'm using the interval variable to store the value from setInterval()
On click of start button, I am able to console.log the value of the interval correctly.
However on click of stop button, interval.current is console logged as undefined. The stopTimer() hence does not function as expected.
Why does interval.current print undefined when it is clearly set in startTimer (and logged there)? What am I missing here?
import React, { useState, useRef } from 'react';
const pad = (time) => {
return time.toString().padStart(2, "0");
};
function App() {
const [title, setTitle] = useState("Pomodoro!");
const [timeLeft, setTimeLeft] = useState(5);
const interval = useRef(null);
const startTimer = () => {
interval.current = setInterval(() => {
setTimeLeft((timeLeft) => {
if (timeLeft >= 1) {
return timeLeft - 1;
}
return 0;
});
}, 1000);
console.log(interval.current, " :in start");
}
const stopTimer = (interval) => {
console.log("in stop: ", interval.current);
clearInterval(interval.current);
}
const resetTimer = () => { }
const minutes = pad(Math.floor(timeLeft / 60));
const seconds = pad((timeLeft - minutes * 60));
return (
<div>
<div>{title}</div>
<div>
<span>{minutes}</span>
<span>:</span>
<span>{seconds}</span>
</div>
<div>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
<button onClick={resetTimer}>Reset</button>
</div>
</div>
);
}
export default App;
output in console
6 " :in start"
in stop: undefined
Thanks
I believe it is because you pass a lower scope variable called interval to stopTimer, but when you call stopTimer you do not pass the argument, so it is undefined when you're accessing it.
You probably referring to interval you've defined as ref so you need to just access it without passing interval to stopTimer, try this:
const stopTimer = () => {
console.log("in stop: ", interval.current);
clearInterval(interval.current);
}
Considering what your code is doing, I believe interval should be a state variable and not a ref. That is to say, you should use
const [interval, setInterval] = useState(null);
instead of const interval = useRef(null);
Refs are used to be linked to DOM elements (for instance a form element you wish to refer to when a button is clicked). It's only when a ref variable is correctly referencing a DOM element that their current attribute is defined.
Related
I am a starter at React! Started last week ;)
My first project is to create a timer which has a reset function and a second count function.
The reset function is working great, however the timer does not. Which is the best way to do it? It should increase +1s on variable 'second' according to the setTimeout() function.
Is it possible to create a loop on Hooks? I tried to do with the code below, but the page goes down, I think it is because the infinite loop that the code creates;
const [hour, setHour] = useState(4)
const [minute, setMinute] = useState(8)
const [second, setSecond] = useState(12)
// methods
const setTime = (value: string) => {
if (value === 'reset'){
setHour(0);
setMinute(0);
setSecond(0);
}
}
const startTime = () => {
while (second < 60){
setTimeout(() => {
setSecond(second + 1);
}, 1000);
}
};
<div className="d-flex justify-content-center">
<MainButton
variantButton="outline-danger"
textButton="RESET"
functionButton={() => setTime('reset')}
/>
<MainButton
variantButton="outline-success"
textButton="START"
functionButton={() => startTime()}
/>
</div>
Welcome to React! You're very close. setTimeout and setInterval are very similar and for this you can simply use setInterval. No need for a while() loop! Check out this working Sandbox where I created a simple React Hook that you can use in your App.js
https://codesandbox.io/s/recursing-hooks-jc6w3v
The reason your code got caught in an infinite loop is because startTime() function has stale props. Specifically, the second variable is always 0 in this case, because when you defined startTime() on component mount, second was 0. The function doesn't track it's incrementing.
To resolve this issue, instead of:
setSecond(second + 1);
Try using:
setSecond((s) => s += 1);
EDIT* There are many good articles on React Stale Props. Here's one that's helpful: https://css-tricks.com/dealing-with-stale-props-and-states-in-reacts-functional-components/
EDIT** Additional inline examples of the exact issue:
Two changes I would make:
Use setInterval instead of setTimeout in a while() loop.
Create a useTimer hook which handles your timer logic.
App.js
import "./styles.css";
import useTimer from "./useTimer";
export default function App() {
const [setTime, startTime, stopTime, hour, minute, second] = useTimer();
return (
<div>
<div className="d-flex justify-content-center">
<button onClick={() => setTime("reset")}>RESET</button>
<button onClick={startTime}>START</button>
<button onClick={stopTime}>STOP</button>
</div>
<br />
<div>
Hour: {hour} <br />
Minute: {minute} <br />
Second: {second} <br />
</div>
</div>
);
}
useTimer.js
import { useState } from "react";
const useTimer = () => {
const [hour, setHour] = useState(4);
const [minute, setMinute] = useState(8);
const [second, setSecond] = useState(12);
const [timer, setTimer] = useState();
// methods
const clearTimer = () => clearInterval(timer);
const setTime = (value) => {
if (value === "reset") {
setHour(0);
setMinute(0);
setSecond(0);
}
};
const startTime = () => {
if (timer) clearTimer();
const newInterval = setInterval(() => {
setSecond((s) => (s += 1));
}, 1000);
setTimer(newInterval);
};
const stopTime = () => clearTimer();
return [setTime, startTime, stopTime, hour, minute, second];
};
export default useTimer;
So I am writing a product prototype in create-react-app, and in my App.js, inside the app() function, I have:
const [showCanvas, setShowCanvas] = useState(true)
This state is controlled by a button with an onClick function; And then I have a function, inside it, the detectDots function should be ran in an interval:
const runFaceDots = async (key, dot) => {
const net = await facemesh.load(...);
setInterval(() => {
detectDots(net, key, dot);
}, 10);
// return ()=>clearInterval(interval);};
And the detectDots function works like this:
const detectDots = async (net, key, dot) => {
...
console.log(showCanvas);
requestFrame(()=>{drawDots(..., showCanvas)});
}
}};
I have a useEffect like this:
useEffect(()=>{
runFaceDots(); return () => {clearInterval(runFaceDots)}}, [showCanvas])
And finally, I can change the state by clicking these two buttons:
return (
...
<Button
onClick={()=>{setShowCanvas(true)}}>
Show Canvas
</Button>
<Button
onClick={()=> {setShowCanvas(false)}}>
Hide Canvas
</Button>
...
</div>);
I checked a few posts online, saying that not clearing interval would cause state loss. In my case, I see some strange behaviour from useEffect: when I use onClick to setShowCanvas(false), the console shows that console.log(showCanvas) keeps switching from true to false back and forth.
a screenshot of the console message
you can see initially, the showCanvas state was true, which makes sense. But when I clicked the "hide canvas" button, and I only clicked it once, the showCanvas was set to false, and it should stay false, because I did not click the "show canvas" button.
I am very confused and hope someone could help.
Try using useCallback for runFaceDots function - https://reactjs.org/docs/hooks-reference.html#usecallback
And ensure you return the setInterval variable to clear the timer.
const runFaceDots = useCallback(async (key, dot) => {
const net = await facemesh.load(...);
const timer = setInterval(() => {
detectDots(net, key, dot);
}, 10);
return timer //this is to be used for clearing the interval
},[showCanvas])
Then change useEffect to this - running the function only if showCanvas is true
useEffect(()=>{
if (showCanvas) {
const timer = runFaceDots();
return () => {clearInterval(timer)}
}
}, [showCanvas])
Update: Using a global timer
let timer // <-- create the variable outside the component.
const MyComponent = () => {
.....
useEffect(()=>{
if (showCanvas) {
runFaceDots(); // You can remove const timer here
return () => {clearInterval(timer)}
} else {
clearInterval(timer) //<-- clear the interval when hiding
}
}, [showCanvas])
const runFaceDots = useCallback(async (key, dot) => {
const net = await facemesh.load(...);
timer = setInterval(() => { //<--- remove const and use global variable
detectDots(net, key, dot);
}, 10);
return timer //this is to be used for clearing the interval
},[showCanvas])
.....
}
I've created a simple timer script initialized with 10 seconds for testing. The problem I'm having is that the pause timer button isn't working as expected.
import React, { useState } from 'react';
function App() {
const [time, updateTime] = useState(() => { return 10 });
const [timerRunning, setTimerRunning] = useState(false);
let minutes = Math.floor(time / 60);
let seconds = time % 60;
let interval;
function startTimer() {
setTimerRunning(true);
interval = setInterval( function() {
updateTime(previousTime => previousTime === 0 ? previousTime : previousTime - 1);
}, 1000);
}
function pauseTimer() {
setTimerRunning(false);
clearInterval(interval);
}
function restartTimer() {
setTimerRunning(false);
updateTime(() => {return 10});
}
return (
<>
<p>{minutes > 9 ? minutes : "0" + minutes}:{seconds > 9 ? seconds : "0" + seconds}</p>
<button onClick={startTimer}>Start</button>
<button onClick={pauseTimer}>Pause</button>
<button onClick={restartTimer}>Restart</button>
</>
)
}
export default App;
I want the pause button to pause the timer. Eventually I'll make conditional statements to have each button appear based on the state of the app and the value of time, but the pause button is my current obstacle.
I first had a separate countdown function which used a conditional to stop the time when the time matched counter (below). I thought of a less complicated way that lets me omit the counter variable (above). Im not sure which option is better, or if either is preventing the clearInterval function to work properly. The clearInterval function works within the countdown function if statement, but will not work outside of it.
import React, { useState } from 'react';
function App() {
const [time, updateTime] = useState(() => { return 10 });
const [timerRunning, setTimerRunning] = useState(false);
let counter = 0;
let minutes = Math.floor(time / 60);
let seconds = time % 60;
let interval;
function countdown() {
counter++;
if ( counter === time ) {
setTimerRunning(false);
clearInterval(interval);
}
updateTime(previousTime => previousTime - 1);
}
function startTimer() {
setTimerRunning(true);
interval = setInterval(countdown, 1000);
}
function pauseTimer() {
setTimerRunning(false);
clearInterval(interval);
}
function restartTimer() {
setTimerRunning(false);
updateTime(() => {return 10});
}
return (
<>
<p>{minutes > 9 ? minutes : "0" + minutes}:{seconds > 9 ? seconds : "0" + seconds}</p>
<button onClick={startTimer}>Start</button>
<button onClick={pauseTimer}>Pause</button>
<button onClick={restartTimer}>Restart</button>
</>
)
}
export default App;
Basically you can't create let interval; and assign it a setInterval like interval = setInterval(countdown, 1000);
because on each re-render there will be new let interval;
what you need to do is create a variable which isn't change on re-redners, you can use useRef
const interval = useRef(null);
.....
function startTimer() {
interval.current = setInterval(countdown, 1000);
}
....
function pauseTimer() {
clearInterval(interval.current);
}
and I don't think you need const [timerRunning, setTimerRunning] = useState(false);
find a demo here
Basically when functional component re-renders it will execute from top to bottom, if you use like let counter = 0;, then on each re-render it will initialize to 0, if you need to persists your values in each re-renders you might need some hooks (Ex: useState, useRef ...), in this case useRef would do the trick (because you need only one setInterval in each re-renders and useRef will give you the previous value, it will not re-initalize like a general variable)
You have to use useEffect, like this:
const handleStart = () => {
setChangeValue(true)
}
const handlePause = () => {
setChangeValue(false)
pauseTimer()
}
const handleRestart = () => {
setInitialState()
setChangeValue(true)
}
useEffect(() => {
if (changeValue) {
const interval = setInterval(() => {
startTimer()
}, 100)
return () => clearInterval(interval)
}
}, [changeValue])
you have three buttons to start, pause and restart, invoke these (handleStart, handlePause, handleRestart) functions with them
that is my solution
instead of the startTime function, I use useEffect
useEffect(()=>{
interval = timerRunning && setInterval(() => {
updateTime(previousTime => previousTime === 0 ? previousTime : previousTime - 1);
}, 1000);
return ()=> clearInterval(interval)
},[timerRunning])
and in onClick Start Button
<button onClick={()=> setTimerRunning(true)}>Start</button>
I hope it is useful
React state value not updated in the console but it is updated in the view.
This is my entire code
import React, { useEffect, useState } from 'react';
const Add = (props) => {
console.log("a = ", props.a)
console.log("b = ", props.b)
const c = props.a+props.b;
return (
<div>
<p><b>{props.a} + {props.b} = <span style={{'color': 'green'}}>{c}</span></b></p>
</div>
)
}
// export default React.memo(Add);
const AddMemo = React.memo(Add);
const MemoDemo = (props) => {
const [a, setA] = useState(10)
const [b, setB] = useState(10)
const [i, setI] = useState(0);
useEffect(() => {
init()
return () => {
console.log("unmounting...")
}
}, [])
const init = () => {
console.log("init", i)
setInterval(()=>{
console.log("i = ", i)
if(i == 3){
setA(5)
setB(5)
}else{
setA(10)
setB(10)
}
setI(prevI => prevI+1)
}, 2000)
}
return (
<div>
<h2>React Memo - demo</h2>
<p>Function returns previously stored output or cached output. if inputs are same and output should same then no need to recalculation</p>
<b>I= {i}</b>
<AddMemo a={a} b={b}/>
</div>
);
}
export default MemoDemo;
Please check this image
Anyone please explain why this working like this and how to fix this
The problem is as you initialized the setInterval once so it would reference to the initial value i all the time. Meanwhile, React always reference to the latest one which always reflect the latest value on the UI while your interval is always referencing the old one. So the solution is quite simple, just kill the interval each time your i has changed so it will reference the updated value:
React.useEffect(() => {
// re-create the interval to ref the updated value
const id = init();
return () => {
// kill this after value changed
clearInterval(id);
};
// watch the `i` to create the interval
}, [i]);
const init = () => {
console.log("init", i);
// return intervalID to kill
return setInterval(() => {
// ...
});
};
In callback passed to setInterval you have a closure on the value of i=0.
For fixing it you can use a reference, log the value in the functional update or use useEffect:
// Recommended
useEffect(() => {
console.log(i);
}, [i])
const counterRef = useRef(i);
setInterval(()=> {
// or
setI(prevI => {
console.log(prevI+1);
return prevI+1;
})
// or
conosole.log(counterRef.current);
}, 2000);
import React from 'react';
import {Plugins} from '#capacitor/core';
import {useState, useEffect} from 'react';
import {db} from './Firebase';
const Maps = () => {
const [lat, setLat] = useState(0);
const [long, setLong] = useState(0);
const [count, setCount] = useState (0);
const Counter = () => {
setCount(count + 1)
console.log(count)
}
const Location = () => {
Plugins.Geolocation.getCurrentPosition().then(
result => setLat ( result.coords.latitude)
)
Plugins.Geolocation.getCurrentPosition().then(
result => setLong (result.coords.longitude)
)
}
const interval = () => {
setInterval (() =>
{
Location();
Counter();
}, 5000 );
}
return (
<div>
<div>
<button onClick = {interval}>
Get Location
</button>
</div>
<div>
{long}
</div>
<div>
{lat}
</div>
</div>
)
}
export default Maps;
I'm trying to get the counter to increment on every iteration of setInterval, through the counter function, but when I log count, it does not increment and always remains as 0.
I've tried running setCount itself within setInterval without any success, it still does not increment count.
Its a stale closure. Change to this setCount(prevCount => prevCount + 1).
Using the updater form of set state like above, you can guarantee that you will be using the most recent value of state.
You can think of it as count in your function being a snapshot of what its value was when the setInterval was declared. This will stop your updates from appearing to work.
In addition, setting state is async, so the console.log(count) will most likely not reflect the new value. Log in an effect or outside the function body to see the updated value each render.
A note about your implementation:
You are creating a setInterval each time the button is clicked. This could lead to some interesting side-effects if clicked more than once. If you click the button twice for example, you will have two setIntervals running every 5 seconds.
In addition to #BrianThompson answer. Try this to avoid innecessary rerenders
import React from 'react';
import {Plugins} from '#capacitor/core';
import {useState, useEffect} from 'react';
import {db} from './Firebase';
const Maps = () => {
const [state, setState] = useState({
latLng:{lat:0,lng:0},
counter: 0
})
const interval = useRef()
//Use camelCase for methods
const location = () => {
Plugins.Geolocation.getCurrentPosition().then(
result => setState ( ({counter}) => {
counter = counter+1
console.log(counter)
return ({
latLng: {
lat: result.coords.latitude,
lng: result.coords.longitude
},
counter
})
})
)
}
const startInterval = () => {
if(interval.current) return;
interval.current = setInterval (() => {
location();
}, 5000 );
}
const stopInterval = () ={
clearInterval(interval.current)
interval.current = null
}
useEffect(()=>{
//Because interval is causing state updates, remember to clear interval when component will unmount
return stopInterval
},[])
return (
<div>
<div>
<button onClick = {startInterval}>
Get Location
</button>
</div>
<div>
{state.latLng.lng}
</div>
<div>
{state.latLng.lat}
</div>
</div>
)
}