Does clearing timeout/interval have to be inside `useEffect` react hook? - reactjs

I'm wondering what is the correct way and best practice to clear timeouts/intervals when using React hooks. For example I have the following code:
import React, { useState, useEffect, useRef } from 'react';
const Test = () => {
const id = useRef(null)
const [count, setCount] = useState(5)
const [timesClicked, setTimesClicked] = useState(0)
if (!count) {
clearInterval(id.current)
}
useEffect(() => {
id.current = setInterval(() => {
setCount(count => count -1)
}, 1000)
return () => {
clearInterval(id.current)
}
}, [])
const onClick = () => setTimesClicked(timesClicked => timesClicked + 1)
return (
<div>countdown: {count >= 0 ? count : 0}
<hr />
Clicked so far: {timesClicked}
{count >= 0 && <button onClick={onClick}>Click</button>}
</div>
)
}
When count equals 0 the interval is cleared in the body of the Test function. In most of the examples I've seen on the Internet interval is cleared inside useEffect, is this mandatory?

You must be sure to clear all intervals before your component gets unmounted.
Intervals never disappear automatically when components get unmounted and to clear them, clearInterval is often called inside useEffect(() => {}, []).
The function retured in useEffect(() => {}, []) gets called when the compoment is unmounted.
return () => {
clearInterval(id.current)
}
You can see that intervals set inside a component never disappears automatically by checking this sandbox link. https://codesandbox.io/s/cool-water-oij8s
Intervals remain forever unless clearInterval is called.

setInterval is a function which is executed repeatedly and it returns an id of the interval. When you call clearInterval with this id, you stop that function from repeating. It's not mandatory to do it inside a certain function, you need to clear it when you no longer want that function to be called subsequently. You can call it in the function you return as a result of useEffect, if that's what you need.

Related

React setInterval is re-initialized to often

I've a hard time understanding how setInterval works. My main problem is that an interval is re-initialized too often.
Basically, I want a context-sensitive sidebar to be modfied by MainElement, and I want this sidebar to do something at a regular base. In the real scenario there the timer gets cancelled when unmounting ofc.
import { useEffect, useState } from 'react';
// This is the component called from outside
export const MainLayout = () => {
const [element2Content, setElement2Content] = useState<string | null>(null);
return (
<>
<MainElement setElement2Content={setElement2Content}>
Element1
</MainElement>
{element2Content && <Sidebar content={element2Content} />}
</>
);
};
// This component manipulates the sidebar via useEffect
const MainElement: React.FC<{ setElement2Content: (input: string) => void }> =
({ setElement2Content, children }) => {
useEffect(() => setElement2Content('content set from element 1'), []);
return <div>{children}</div>;
};
// This component utilizes the setInterval, but doesn't work as expected
const Sidebar: React.FC<{ content: string }> = ({ content }) => {
const [calls, setCalls] = useState(0);
useEffect(() => {
setInterval(() => {
console.log('interval called for', calls + 1, 'times');
setCalls(calls + 1);
}, 1000);
}, []);
return <div>{`content${content}, calls: ${calls}`}</div>;
};
The log is just interval called for 1 times in a loop.
In the browser I see the components rendered, and I see interval called for 0 times being changed to interval called for 1 times, where it stops.
So I'm wondering: Why does it stop at 1? It seems like setInterval gets reset all the time.
To understand the behavior of a timer a bit more, I changed my MainElement to
const MainElement: React.FC<{ setElement2Content: (input: string) => void }> =
({ setElement2Content, children }) => {
useEffect(() => setElement2Content('content set in element 1'), []);
useEffect(() => {
setInterval(() => {
console.log('interval called from mainelement');
}, 1000);
}, []);
return <div>{children}</div>;
};
Now, for some reason the MainElement is also re-rendered repeatedly, and so is the sidebar. The console logs
interval called in mainelement
interval called for 1 times
I'ld be grateful for any ideas or explanations!
The interval is running correctly, but in your interval function calls is never being updated passed 1. This is because the calls variable in the setInterval function is stale (i.e. it is always equal to 0 when the function is invoked, so setCalls(call + 1) will always update calls to 1.) This staleness is because the calls variable in the function is captured in the closure formed when the effect is run during the first render of the component and never updated thereafter.
A quick way to address this is to use the functional version of the setState call:
useEffect(() => {
setInterval(() => {
console.log('interval called for', calls + 1, 'times');
setCalls(prevCalls => prevCalls + 1);
}, 1000);
}, []);

Is it okay to change boolean which is a hook and dependency of useEffect in React?

I am working on CSS Transition on React that it automatically unmount after 2 seconds. I am thinking about using useEffect and useState to solve this problem.
I know that changing dependencies inside useEffect causes infinite loop.
For example, the code below will cause infinite loop.
const [count, setCount] = useState(0);
useEffect(() => {
setCount(prev => prev + 1);
},[count]);
But I think infinite loop won't happen if I set dependency to boolean and set if statement inside useEffect just like the code below.
const [showStatus, setshowStatus] = useState(false);
useEffect(() => {
const timeId = setTimeout(() => {
if (showStatus === true){
setshowStatus(false)
}
}, 2000)
return (() => {clearTimeout(timeId)})
}, [showStatus]);
I am relatively new to React so I am worried about this code. Do I have any problems using this code?
I think it's answered here better
React hooks - right way to clear timeouts and intervals
import { useState, useEffect } from "react";
const delay = 5;
export default function App() {
const [show, setShow] = useState(false);
useEffect(
() => {
let timer1 = setTimeout(() => setShow(true), delay * 1000);
// this will clear Timeout
// when component unmount like in willComponentUnmount
// and show will not change to true
return () => {
clearTimeout(timer1);
};
},
// useEffect will run only one time with empty []
// if you pass a value to array,
// like this - [data]
// than clearTimeout will run every time
// this value changes (useEffect re-run)
[]
);
return show ? (
<div>show is true, {delay}seconds passed</div>
) : (
<div>show is false, wait {delay}seconds</div>
);
}

Counter in react-js run only once when Component Re-Render?

I tried setting a timer to a function I want to be called every 2 seconds:
// start timer
if(!timerStarted){
tid = setInterval(ReloadMessage, 2000);
timerStarted = true
}
But I want this timer instance to only be ran once (hence the !timerStarted)
Unfortunately, this gets ignored when the component rerenders from the state change.
I tried ending the timer but I found no way to know in advance when the state changes.
So I tried:
//my Functional component useEffect
React.useEffect(()=>{
(async () => {
// start timer
if(!timerStarted){
tid = setInterval(ReloadMessage, 2000);
timerStarted = true
}
})()
},[])
Thinking this would make the effect be called only once upon component load, but this ended up not calling the timer at all (Maybe because I also have a second effect with dependencies here?)
How do I make sure this timer is set off once and only once, no matter what the user does?
Using an empty dependencies array for your effect, will ensure that it only runs once. With that in mind, it's kind of irrelevant to track that a timerStarted.
The usage of this flag (provided it's a variable scoped to the component) even indicates that it actually should be a dependency, which your linter, if you have one, should notify you of. Though as stated above you don't need it, and it would only make things more complicated.
Also the async IIEF is not needed as you don't await anything.
So, all in all, this should be enough:
React.useEffect(()=>{
const tid = setInterval(ReloadMessage, 2000);
return () => {
clearInterval(tid);
};
},[]);
As per the comments, here's a simple demo of how you can use a ref, to get access to some dependency that you absolutely do not want to list as a dependency. Use this sparingly and only with good consideration, because it often hints at a problem that started somewhere else (often a design problem):
import { useEffect, useRef, useState } from 'react';
const Tmp = () => {
const [counter, setCounter] = useState(0);
const counterRef = useRef(counter);
useEffect(() => {
counterRef.current = counter;
}, [counter]);
useEffect(() => {
const t = setInterval(() => {
console.log('Invalid', counter); // always *lags behind* because of *closures* and
// will trigger a linter error, as it should actually be a dependency
console.log('Valid', counterRef.current); // current counter
}, 2000);
return () => {
clearInterval(t);
};
}, []);
return (
<div>
<div>
<button onClick={() => setCounter(current => current - 1)}>-</button>
{counter}
<button onClick={() => setCounter(current => current + 1)}>+</button>
</div>
</div>
);
};
export default Tmp;

React state not updating inside setInterval

I'm trying to learn React with some simple projects and can't seem to get my head around the following code, so would appreciate an explanation.
This snippet from a simple countdown function works fine; however, when I console.log, the setTime appears to correctly update the value of 'seconds', but when I console.log(time) immediately after it gives me the original value of 3. Why is this?
Bonus question - when the function startCountdown is called there is a delay in the correct time values appearing in my JSX, which I assume is down to the variable 'seconds' being populated and the start of the setInterval function, so I don't get a smooth and accurate start to the countdown. Is there a way around this?
const [ time, setTime ] = useState(3);
const [ clockActive, setClockActive ] = useState(false);
function startCountdown() {
let seconds = time * 60;
setClockActive(true);
let interval = setInterval(() => {
setTime(seconds--);
console.log(seconds); // Returns 179
console.log(time); // Returns 3
if(seconds < 0 ) {
clearInterval(interval);
}
}, 1000)
};
Update:
The reason you are not seeing the correct value in your function is the way that setState happens(setTime). When you call setState, it batches the calls and performs them when it wants to in the background. So you cannot call setState then immediately expect to be able to use its value inside of the function.
You can Take the console.log out of the function and put it in the render method and you will see the correct value.
Or you can try useEffect like this.
//This means that anytime you use setTime and the component is updated, print the current value of time. Only do this when time changes.
useEffect(()=>{
console.log(time);
},[time]);
Every time you setState you are rerendering the component which causes a havoc on state. So every second inside of your setInterval, you are re-rendering the component and starting it all over again ontop of what you already having running. To fix this, you need to use useEffect and pass in the state variables that you are using. I did an example for you here:
https://codesandbox.io/s/jolly-keller-qfwmx?file=/src/clock.js
import React, { useState, useEffect } from "react";
const Clock = (props) => {
const [time, setTime] = useState(3);
const [clockActive, setClockActive] = useState(false);
useEffect(() => {
let seconds = 60;
setClockActive(true);
const interval = setInterval(() => {
setTime((time) => time - 1);
}, 1000);
if (time <= 0) {
setClockActive(false);
clearInterval(interval);
}
return () => {
setClockActive(false);
clearInterval(interval);
};
}, [time, clockActive]);
return (
<>
{`Clock is currently ${clockActive === true ? "Active" : "Not Active"}`}
<br />
{`Time is ${time}`}
</>
);
};
export default Clock;

How to deal with stale state values inside of a useEffect closure?

The following example is of a Timer component that has a button (to start the timer), and two tags that display the number of elapsed seconds, and the number of elapsed seconds times 2.
However, it does not work (CodeSandbox Demo)
The Code
import React, { useState, useEffect } from "react";
const Timer = () => {
const [doubleSeconds, setDoubleSeconds] = useState(0);
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
console.log("Creating Interval");
setSeconds((prev) => prev + 1);
setDoubleSeconds(seconds * 2);
}, 1000);
} else {
clearInterval(interval);
}
return () => {
console.log("Destroying Interval");
clearInterval(interval);
};
}, [isActive]);
return (
<div className="app">
<button onClick={() => setIsActive((prev) => !prev)} type="button">
{isActive ? "Pause Timer" : "Play Timer"}
</button>
<h3>Seconds: {seconds}</h3>
<h3>Seconds x2: {doubleSeconds}</h3>
</div>
);
};
export { Timer as default };
The Problem
Inside the useEffect call, the "seconds" value will always be equal to the its value when the useEffect block was last rendered (when isActive last changed). This will result in the setDoubleSeconds(seconds * 2) statement to fail. The React Hooks ESLint plugin gives me a warning regarding this problem that reads:
React Hook useEffect has a missing dependency: 'seconds'. Either include it or remove the dependency array. You can also replace
multiple useState variables with useReducer if 'setDoubleSeconds'
needs the current value of 'seconds'.
(react-hooks/exhaustive-deps)eslint
And correctly so, adding "seconds" to the dependency array (and changing setDoubleSeconds(seconds * 2) to setDoubleSeconds((seconds + 1) * ) will render the correct results. However, this has a nasty side effect of causing the interval to be created and destroyed on every render (the console.log("Destroying Interval") fires on every render).
So now I am looking at the other recommendation from the ESLint warning "You can also replace multiple useState variables with useReducer if 'setDoubleSeconds' needs the current value of 'seconds'".
I do not understand this recommendation. If I create a reducer and use it like so:
import React, { useState, useEffect, useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case "SET": {
return action.seconds;
}
default: {
return state;
}
}
};
const Timer = () => {
const [doubleSeconds, dispatch] = useReducer(reducer, 0);
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
console.log("Creating Interval");
setSeconds((prev) => prev + 1);
dispatch({ type: "SET", seconds });
}, 1000);
} else {
clearInterval(interval);
}
return () => {
console.log("Destroying Interval");
clearInterval(interval);
};
}, [isActive]);
return (
<div className="app">
<button onClick={() => setIsActive((prev) => !prev)} type="button">
{isActive ? "Pause Timer" : "Play Timer"}
</button>
<h3>Seconds: {seconds}</h3>
<h3>Seconds x2: {doubleSeconds}</h3>
</div>
);
};
export { Timer as default };
The problem of stale values will still exist (CodeSandbox Demo (using Reducers)).
The Question(s)
So what is the recommendation for this scenario? Do I take the performance hit and simply add "seconds" to the dependency array? Do I create another useEffect block that depends on "seconds" and call "setDoubleSeconds()" in there? Do I merge "seconds" and "doubleSeconds" into a single state object? Do I use refs?
Also, you might be thinking "Why don't you simply change <h3>Seconds x2: {doubleSeconds}</h3>" to <h3>Seconds x2: {seconds * 2}</h3> and remove the 'doubleSeconds' state?". In my real application doubleSeconds is passed to a Child component and I do not want the Child component to know how seconds is mapped to doubleSeconds as it makes the Child less re-usable.
Thanks!
You can access a value inside an effect callback without adding it as a dep in a few ways.
setState. You can tap the up-to-date value of a state variable through its setter.
setSeconds(seconds => (setDoubleSeconds(seconds * 2), seconds));
Ref. You can pass a ref as a dependency and it'll never change. You need to manually keep it up to date, though.
const secondsRef = useRef(0);
const [seconds, setSeconds] = useReducer((_state, action) => (secondsRef.current = action), 0);
You can then use secondsRef.current to access seconds in a block of code without having it trigger deps changes.
setDoubleSeconds(secondsRef.current * 2);
In my opinion you should never omit a dependency from the deps array. Use a hack like the above to make sure your values are up-to-date if you need the deps not to change.
Always first consider if there's some more elegant way to write your code than hacking a value into a callback. In your example doubleSeconds can be expressed as a derivative of seconds.
const [seconds, setSeconds] = useState(0);
const doubleSeconds = seconds * 2;
Sometimes applications aren't that simple so you may need to use the hacks described above.
Do I take the performance hit and simply add "seconds" to the dependency array?
Do I create another useEffect block that depends on "seconds" and call "setDoubleSeconds()" in there?
Do I merge "seconds" and "doubleSeconds" into a single state object?
Do I use refs?
All of them work correctly, although personally I would rather choose the second approach:
useEffect(() => {
setDoubleSeconds(seconds * 2);
}, [seconds]);
However:
In my real application doubleSeconds is passed to a Child component and I do not want the Child component to know how seconds is mapped to doubleSeconds as it makes the Child less re-usable
That is questionable. Child component might be implemented like the following:
const Child = ({second}) => (
<p>Seconds: {second}s</p>
);
And parent component should look like the following:
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// change seconds
}, []);
return (
<React.Fragment>
<Child seconds={second} />
<Child seconds={second * 2} />
</React.Fragment>
);
This would be a more clear and concise way.

Resources