why I can't stop interval in react functional component - javascript

In my code I have some problems with intervals. I have situation like this
import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
function handleClick() {
const timer = setInterval(() => {
if (count >= 10) {
clearInterval(timer);
} else {
setCount((prevCount) => prevCount + 1);
}
}, 1000);
}
return (
<>
<button onClick={handleClick}>start</button>
<p>{count}</p>
</>
);
}
export default App;
what am trying to do is to start an interval when user click a button and stop it when counter reaches 10, but it never stops and debugger says that inside setInterval count is always 0. Does anybody know what is the problem? I also found that if I rewrite component to class component like this
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
handleClick() {
let myInterval = setInterval(() => {
if (this.state.count >= 10) {
clearInterval(myInterval);
} else {
this.setState((prevState) => ({
count: prevState.count + 1,
}));
}
}, 1000);
}
render() {
return (
<>
<button onClick={this.handleClick.bind(this)}>start</button>
<p>{this.state.count}</p>
</>
);
}
}
it works perfectly.
I don't know what's happening and I spend almost day trying to figure out.
So does anybody know why class component works and why functional component doesn't?
Thanks in advance

You're seeing a stale count in the setInterval callback function since it captures the value at the time you're starting the timer.
You can use the "ref boxing" pattern to have a readable reference to the latest value of the state atom:
import {useState, useRef} from 'react';
function App() {
const [count, setCount] = useState(0);
const countRef = useRef(null);
countRef.current = count; // Update the boxed value on render
function handleClick() {
const timer = setInterval(() => {
if (countRef.current >= 10) { // Read the boxed value
clearInterval(timer);
} else {
setCount((prevCount) => prevCount + 1);
}
}, 1000);
}
return (
<>
<button onClick={handleClick}>start</button>
<p>{count}</p>
</>
);
}
Note that you're currently not correctly cleaning up the timer on unmount, leading to React warning you about that when you'd unmount your app. The same holds for your class component.

Related

functional component still rerender with react.memo

I want to make a quiz app with a countdown timer.
The timer use setinterval and the quiz has a functional component for Question.
I have already set react.memo for the question function component. But it keeps rerender each second. It works fine on desktop. But it flickers on mobile since the input for answer keep refocus
import { useEffect, useState, memo } from 'react';
function Game() {
let interval;
const [timer, setTimer] = useState(180000);
useEffect(() => {
interval = setInterval(() => {
setTimer(timer - 1000);
}, 1000);
return () => {
if (interval) {
clearInterval(interval)
}
}
}, [timer]);
const QuestionComponent = memo(props => {
return(<div></div>);
});
return (
<>
<div>{timer}</div>
<QuestionComponent/>
</>
);
}
The above code is the Game component, which will be called from App component
The question component keeps rerendering even there is nothing in it.
May anyone please advise? Thank you very much
Ideally you should define your components outside your other components. As mentioned in comment, this line of code will run on every render.
Even if you use React.memo(), this line itself will run on every render, in contrast to writing it outside where it will run normally.
If you are writing it inside, you have to persist the value between renders. It can be done using useCallback because a react component is technically a function:
const Component = useCallback(
memo(() => {
console.log("rerender");
return <div></div>;
}),
[]
);
Or another way is using a ref.
import { useEffect, useState, memo, useMemo, useCallback, useRef } from "react";
function App() {
let interval;
const [timer, setTimer] = useState(180000);
const componentRef = useRef();
useEffect(() => {
interval = setInterval(() => {
setTimer(timer - 1000);
}, 1000);
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [timer]);
useEffect(() => {
const Component = memo(() => {
console.log("rerender");
return <div></div>;
});
componentRef.current = Component;
}, []);
return (
<>
<div>{timer}</div>
<>{componentRef.current && <componentRef.current />}</>
</>
);
}
export default App;
Link
But both above are hacks. And you will definitely run into issues when you use hooks.
Much better to just write it outside.
const QuestionComponent = memo(props => {
return(<div></div>);
});
import { useEffect, useState, memo } from 'react';
function Game() {
let interval;
const [timer, setTimer] = useState(180000);
useEffect(() => {
interval = setInterval(() => {
setTimer(timer - 1000);
}, 1000);
return () => {
if (interval) {
clearInterval(interval)
}
}
}, [timer]);
return (
<>
<div>{timer}</div>
<QuestionComponent/>
</>
);
}

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;

Component doesn't update once the parents state is changed

I'm attempting to get a timer to call a function quickly at first and then slow down. I have a TimeInterval state that increases and is passed down to my countdown component as a prop
<Countdown
isActive={RandominatorRunning}
target={() => NextSelection()}
timeToChange={TimeInterval}
/>
Countdown Component
import React, { useEffect } from 'react';
const Countdown = ({ isActive, target, timeToChange }) => {
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
target()
}, timeToChange)
} else if (!isActive) {
clearInterval(interval)
}
return () => clearInterval(interval)
}, [isActive])
return null
}
export default Countdown;
My TimeInterval state is working properly and will increase as NextSelection() is called. However this doesn't seem to increase the interval of the countdown component and NextSelection() is always called at the same pace, not at the changing state TimeInterval pace. Why is the countdown component not updating it's pace along with the TimeInterval state?
Not positive this is the best solution, but I was able to alter my countdown component to get the desired effect.
I changed my countdown component to become inactive while it executes the prop update, then resumes as soon as the prop has finished updating.
import React, { useEffect, useState } from 'react';
const Countdown = ({ isActive, target, timeToChange }) => {
const [Active, setActive] = useState(isActive)
const handleTimeExpire = async () => {
await target()
setActive(true)
}
useEffect(() => {
let interval = null;
if (Active) {
interval = setInterval(() => {
setActive(false)
handleTimeExpire()
}, timeToChange)
} else if (!Active) {
clearInterval(interval)
}
return () => clearInterval(interval)
}, [Active])
return null
}
export default Countdown;

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 correctly work with data from React Context in useEffect/useCallback-hook

I'm using a React Context to store data and to provide functionality to modify these data.
Now, I'm trying to convert a Class Component into a Functional Component using React Hooks.
While everything is working as expected in the Class, I don't get it to work in the Functional Component.
Since my applications code is a bit more complex, I've created this small example (JSFiddle link), which allows to reproduce the problem:
First the Context, which is the same for both, the Class and the Functional Component:
const MyContext = React.createContext();
class MyContextProvider extends React.Component {
constructor (props) {
super(props);
this.increase = this.increase.bind(this);
this.reset = this.reset.bind(this);
this.state = {
current: 0,
increase: this.increase,
reset: this.reset
}
}
render () {
return (
<MyContext.Provider value={this.state}>
{this.props.children}
</MyContext.Provider>
);
}
increase (step) {
this.setState((prevState) => ({
current: prevState.current + step
}));
}
reset () {
this.setState({
current: 0
});
}
}
Now, here is the Class component, which works just fine:
class MyComponent extends React.Component {
constructor (props) {
super(props);
this.increaseByOne = this.increaseByOne.bind(this);
}
componentDidMount () {
setInterval(this.increaseByOne, 1000);
}
render () {
const count = this.context;
return (
<div>{count.current}</div>
);
}
increaseByOne () {
const count = this.context;
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}
}
MyComponent.contextType = MyContext;
The expected result is, that it counts to 5, in an interval of one second - and then starts again from 0.
And here is the converted Functional Component:
const MyComponent = (props) => {
const count = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
console.log(count.current);
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}, []);
React.useEffect(() => {
setInterval(increaseByOne, 1000);
}, [increaseByOne]);
return (
<div>{count.current}</div>
);
}
Instead of resetting the counter at 5, it resumes counting.
The problem is, that count.current in line if (count.current === 5) { is always 0, since it does not use the latest value.
The only way I get this to work, is to adjust the code on the following way:
const MyComponent = (props) => {
const count = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
console.log(count.current);
if (count.current === 5) {
count.reset();
}
else {
count.increase(1);
}
}, [count]);
React.useEffect(() => {
console.log('useEffect');
const interval = setInterval(increaseByOne, 1000);
return () => {
clearInterval(interval);
};
}, [increaseByOne]);
return (
<div>{count.current}</div>
);
}
Now, the increaseByOne callback is recreated on every change of the context, which also means that the effect is called every second.
The result is, that it clears the interval and sets a new one, on every change to the context (You can see that in the browser console).
This may work in this small example, but it changed the original logic, and has a lot more overhead.
My application does not rely on an interval, but it's listening for an event. Removing the event listener and adding it again later, would mean, that I may loose some events, if they are fired between the remove and the binding of the listener, which is done asynchronously by React.
Has someone an idea, how it is expected to React, to solve this problem without to change the general logic?
I've created a fiddle here, to play around with the code above:
https://jsfiddle.net/Jens_Duttke/78y15o9p/
First solution is to put data is changing through time into useRef so it would be accessible by reference not by closure(as well as you access actual this.state in class-based version)
const MyComponent = (props) => {
const countByRef = React.useRef(0);
countByRef.current = React.useContext(MyContext);
React.useEffect(() => {
setInterval(() => {
const count = countByRef.current;
console.log(count.current);
if (count.current === 5) {
count.reset();
} else {
count.increase(1);
}
}, 1000);
}, []);
return (
<div>{countByRef.current.current}</div>
);
}
Another solution is to modify reset and increase to allow functional argument as well as it's possible with setState and useState's updater.
Then it would be
useEffect(() => {
setInterval(() => {
count.increase(current => current === 5? 0: current + 1);
}, 1000);
}, [])
PS also hope you have not missed clean up function in your real code:
useEffect(() => {
const timerId = setInterval(..., 1000);
return () => {clearInterval(timerId);};
}, [])
otherwise you will have memory leakage
If the increaseByOne function doesn't need to know the actual count.current, you can avoid recreating it. In the context create a new function called is that checks if the current is equal a value:
is = n => this.state.current === n;
And use this function in the increaseByOne function:
if (count.is(5)) {
count.reset();
}
Example:
const MyContext = React.createContext();
class MyContextProvider extends React.Component {
render() {
return (
<MyContext.Provider value={this.state}>
{this.props.children}
</MyContext.Provider>
);
}
increase = (step) => {
this.setState((prevState) => ({
current: prevState.current + step
}));
}
reset = () => {
this.setState({
current: 0
});
}
is = n => this.state.current === n;
state = {
current: 0,
increase: this.increase,
reset: this.reset,
is: this.is
};
}
const MyComponent = (props) => {
const { increase, reset, is, current } = React.useContext(MyContext);
const increaseByOne = React.useCallback(() => {
if (is(5)) {
reset();
} else {
increase(1);
}
}, [increase, reset, is]);
React.useEffect(() => {
setInterval(increaseByOne, 1000);
}, [increaseByOne]);
return (
<div>{current}</div>
);
}
const App = () => (
<MyContextProvider>
<MyComponent />
</MyContextProvider>
);
ReactDOM.render( <
App / > ,
document.querySelector("#app")
);
body {
background: #fff;
padding: 20px;
font-family: Helvetica;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="app"></div>

Categories

Resources