How to clean up setInterval in useEffect using react hooks - javascript

I am trying to create a loading component that will add a period to a div periodically, every 1000ms using setInterval in React. I am trying to cleanup setInterval using the method described in the docs.
https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1
import React, { useEffect, useState } from 'react'
const Loading = () => {
const [loadingStatus, setLoadingStatus] = useState('.')
const [loop, setLoop] = useState()
useEffect(() => {
setLoop(setInterval(() => {
console.log("loading")
setLoadingStatus(loadingStatus + ".")
}, 1000))
return function cleanup() {
console.log('cleaning up')
clearInterval(loop)
}
}, [])
return (<p>
{`Loading ${loadingStatus}`}
</p>)
}
export default Loading
However , the loadingStatus variable only updates once and the setInterval loop doesnt get cleared even after the component stops mounting. Do I have to make this using a class component?

Dependencies are our hint for React of when the effect should run, even though we set an interval and providing no dependencies [], React wont know we want to run it more then once because nothing really changes in our empty dependencies [].
To get the desired result we need to think when we want to run the effect ?
We want to run it when loadingStatus changes, so we need to add loadingStatus as our dependency because we want to run the effect every time loadingStatus changes.
We have 2 options
Add loadingStatus as our dependency.
const Loading = () => {
const [loadingStatus, setLoadingStatus] = useState(".");
const [loop, setLoop] = useState();
useEffect(
() => {
setLoop(
setInterval(() => {
console.log("loading");
setLoadingStatus(loadingStatus + ".");
}, 1000)
);
return function cleanup() {
console.log("cleaning up");
clearInterval(loop);
};
},
[loadingStatus]
);
return <p>{`Loading ${loadingStatus}`}</p>;
};
Make our effect not aware that we use loadingStatus
const Loading = () => {
const [loadingStatus, setLoadingStatus] = useState(".");
useEffect(() => {
const intervalId = setInterval(() => {
setLoadingStatus(ls => ls + ".");
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <p>{`Loading ${loadingStatus}`}</p>;
};
Read more here => a-complete-guide-to-useeffect

Related

Cannot retrieve current state inside async function in React.js

I have created some state at the top level component (App), however it seems that when this state is updated, the updated state is not read by the asynchronous function defined in useEffect() (it still uses the previous value), more detail below:
I am attempting to retrieve the state of the const processing in the async function toggleProcessing defined in useEffect(), so that when processing becomes false, the async function exits from the while loop. However, it seems that when the processing updates to false, the while loop still keeps executing.
The behaviour should be as follows: Pressing the 'Begin Processing' button should console log "Processing..." every two seconds, and when that same button is pressed again (now labeled 'Stop Processing'), then "Stopping Processing" should be console logged. However, in practice, "Stopping Processing" is never console logged, and "Processing" is continuously logged forever.
Below is the code:
import React, { useState, useEffect} from 'react'
const App = () => {
const [processing, setProcessing] = useState(false)
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms))
}
useEffect(() => {
const toggleProcessing = async () => {
while (processing) {
console.log('Processing...')
await sleep(2000);
}
console.log('Stopping Processing')
}
if (processing) {
toggleProcessing() // async function
}
}, [processing])
return (
<>
<button onClick={() => setProcessing(current => !current)}>{processing ? 'Stop Processing' : 'Begin Processing'}</button>
</>
)
}
export default App;
It really just comes down to being able to read the updated state of processing in the async function, but I have not figure out a way to do this, despite reading similar posts.
Thank you in advance!
If you wish to access a state when using timeouts, it's best to keep a reference to that variable. You can achieve this using the useRef hook. Simply add a ref with the processing value and remember to update it.
const [processing, setProcessing] = useState<boolean>(false);
const processingRef = useRef(null);
useEffect(() => {
processingRef.current = processing;
}, [processing]);
Here is the working code:
import React, { useState, useEffect, useRef} from 'react'
const App = () => {
const [processing, setProcessing] = useState(false)
const processingRef = useRef(null);
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms))
}
useEffect(() => {
const toggleProcessing = async () => {
while (processingRef.current) {
console.log('Processing')
await sleep(2000);
}
console.log('Stopping Processing')
}
processingRef.current = processing;
if (processing) {
toggleProcessing() // async function
}
}, [processing])
return (
<>
<button onClick={() => setProcessing(current => !current)}>{processing ? 'Stop Processing' : 'Begin Processing'}</button>
</>
)
}
export default App;
I was interested in how this works and exactly what your final solution was based on the accepted answer. I threw together a solution based on Dan Abramov's useInterval hook and figured this along with a link to some related resources might be useful to others.
I'm curious, is there any specific reason you decided to use setTimeout and introduce async/await and while loop rather than use setInterval? I wonder the implications. Will you handle clearTimeout on clean up in the effect if a timer is still running on unmount? What did your final solution look like?
Demo/Solution with useInterval
https://codesandbox.io/s/useinterval-example-processing-ik61ho
import React, { useState, useEffect, useRef } from "react";
const App = () => {
const [processing, setProcessing] = useState(false);
useInterval(() => console.log("processing"), 2000, processing);
return (
<div>
<button onClick={() => setProcessing((prev) => !prev)}>
{processing ? "Stop Processing" : "Begin Processing"}
</button>
</div>
);
};
function useInterval(callback, delay, processing) {
const callbackRef = useRef();
// Remember the latest callback.
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
callbackRef.current();
}
if (delay !== null && processing) {
let id = setInterval(tick, delay);
console.log(`begin processing and timer with ID ${id} running...`);
// Clear timer on clean up.
return () => {
console.log(`clearing timer with ID ${id}`);
console.log("stopped");
clearInterval(id);
};
}
}, [delay, processing]);
}
export default App;
Relevant Links
Dan Abramov - Making setInterval Declarative with React Hooks
SO Question: React hooks - right way to clear timeouts and intervals
setTimeout and clearTimeout in React with Hooks (avoiding memory leaks by clearing timers on clean up in effects)

Assignments to the 'timeInterval' variable from inside React Hook useEffect will be lost after each render

I need the auto re-rendering every 6s and I defined the component like the following.
const KioskPage = () => {
const [time, setTime] = useState(Date.now())
useEffect(() => {
timeInterval = setInterval(() => setTime(Date.now()), 60000)
return () => {
clearInterval(timeInterval)
}
}, [])
}
but I got the notification :
Assignments to the 'timeInterval' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect react-hooks/exhaustive-deps
Why this happen? and How can I fix this issue?
Regards
Assignments to the 'timeInterval' variable from inside React Hook useEffect will be lost after each render.
To illustrate:
const KioskPage = () => {
const [time, setTime] = useState(Date.now())
let timeInterval;
// ^^^^^ this piece of code gets run on each render, when state/prop changes.
// this will, in practice, clear the `timeInterval` value set by your effect below, when the component re-renders after being mounted.
useEffect(() => {
timeInterval = setInterval(() => setTime(Date.now()), 60000)
// ^^^^ this assignment gets run once when the component mounts
return () => {
clearInterval(timeInterval)
}
}, []);
return (/* render something*/);
}
You can fix this by, as suggested, "move this variable directly inside useEffect".
const KioskPage = () => {
const [time, setTime] = useState(Date.now())
useEffect(() => {
const timeInterval = setInterval(() => setTime(Date.now()), 6000);
// ^^^^^^^^^^^^^^^
return () => {
console.log('clearing!');
clearInterval(timeInterval)
}
}, []);
const formatted = new Date(time).toLocaleTimeString();
return (
<h1>Time: {formatted}</h1>
);
}

Debounce in React component JS

I'm writing a simple debounce function for an input component
export const debounce = (func, wait) => {
let timeout
return function (...args) {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
timeout = null
Reflect.apply(func, this, args)
}, wait)
}
}
it imported from an external file, and used as a wrap for input onKeyUp handler inside a React component (Hooks)
const handleChange = debounce(() => console.log("test"), 1000)
PROBLEM: I'm getting "test" log every time when text in input changes, not only one - as expected.
What am I doing wrong?
I'm not sure what is the problem with your code but here is a version with hooks working
import { useEffect, useState } from "react";
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
};
export default useDebounce;
and then you use it as
const debouncedValue = useDebounce(inputValue, delay);

Why is the state not being properly updated in this React Native component?

I have a React Native component being used on multiple screens in which I use a reoccurring setTimeout function to animate a carousel of images. The carousel works great, but I want to properly clear out the timer when the screen is navigated away from in the callback function returned from the useEffect hook. (If I don't clear out the timer, then I get a nasty error, and I know I'm supposed to clean up timers anyway.)
For whatever reason though, the state variable I'm trying to set the setTimeout returned timeout ID to seems to be set to null in the callback returned from useEffect.
Here's a simplified version of my code:
const Carousel = () => {
const [timeoutId, setTimeoutId] = useState(null);
const startCarouselCycle = () => {
const newTimeoutId = setTimeout(() => {
// Code here that calls scrollToIndex for the FlatList.
}, 5000);
setTimeoutId(newTimeoutId);
};
const startNextCarouselCycle = () => {
// Other code here.
startCarouselCycle();
};
useEffect(() => {
startCarouselCycle();
return () => {
// This is called when the screen with the carousel
// is navigated away from, but timeoutId is null.
// Why?!
clearTimeout(timeoutId);
};
}, []);
return (
<FlatList
// Non-essential code removed.
horizontal={true}
scrollEnabled={false}
onMomentumScrollEnd={startNextCarouselCycle}
/>
);
};
export default Carousel;
Does anyone have any idea why the state would not be properly updating for use in the returned useEffect callback? Thank you.
You have to remove the dependency array from your useEffect hook like this:
useEffect(() => {
startCarouselCycle();
return () => {
// This is called when the screen with the carousel
// is navigated away from, but timeoutId is null.
// Why?!
clearTimeout(timeoutId);
};
});
This is because your effect is triggered once when the component mounts and it only gets the initial value of your timeoutId.
Based on what I've seen I don't think it's necessary to store your timeout ID in state. Try this:
import React, { useState, useEffect } from 'react';
import { FlatList } from 'react-native';
const Carousel = () => {
let _timeoutId = null
const startCarouselCycle = () => {
const newTimeoutId = setTimeout(() => {
// Code here that calls scrollToIndex for the FlatList.
}, 5000);
_timeoutId = newTimeoutId;
};
const startNextCarouselCycle = () => {
// Other code here.
startCarouselCycle();
};
useEffect(() => {
startCarouselCycle();
return () => {
// This is called when the screen with the carousel
// is navigated away from, but timeoutId is null.
// Why?!
clearTimeout(_timeoutId);
};
}, []);
return (
<FlatList
// Non-essential code removed.
horizontal={true}
scrollEnabled={false}
onMomentumScrollEnd={startNextCarouselCycle} />
);
};
export default Carousel;
Thanks to everyone for your answers and feedback. I tried implementing what everyone recommended, but to no avail. Luckily, they started me off on the right path, and for all I know, maybe there was something else in my component that I didn't mention in my question that was causing things to be more complex.
All the same, after banging my head against a wall for days, I was able to resolve this problem with the following:
let timeoutId;
const Carousel = () => {
const startCarouselCycle = () => {
timeoutId = setTimeout(() => {
// Code here that calls scrollToIndex for the FlatList.
}, 5000);
};
const startNextCarouselCycle = () => {
// Other code here.
startCarouselCycle();
};
useEffect(() => {
startCarouselCycle();
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, []);
return (
<FlatList
// Non-essential code removed.
horizontal={true}
scrollEnabled={false}
onMomentumScrollEnd={startNextCarouselCycle}
/>
);
};
export default Carousel;
The main thing I changed was moving the timeoutId variable to outside the component render function. The render function is getting continuously called, which was causing the timeoutId to not properly update (no clue why; some closure issue?!).
All the same, moving the variable outside the Carousel function did the trick.

What's useEffect execution order and its internal clean-up logic when requestAnimationFrame and cancelAnimationFrame are used?

According to react document, useEffect will trigger clean-up logic before it re-runs useEffect part.
If your effect returns a function, React will run it when it is time to clean up...
There is no special code for handling updates because useEffect handles them by default. It cleans up the previous effects before applying the next effects...
However, when I use requestAnimationFrame and cancelAnimationFrame inside useEffect, I found the cancelAnimationFrame may not stop the animation normally. Sometimes, I found the old animation still exists, while the next effect brings another animation, which causes my web app performance issues (especially when I need to render heavy DOM elements).
I don't know whether react hook will do some extra things before it executes the clean-up code, which make my cancel-animation part not work well, will useEffect hook do something like closure to lock the state variable?
What's useEffect's execution order and its internal clean-up logic? Is there something wrong the code I write below, which makes cancelAnimationFrame can't work perfectly?
Thanks.
//import React, { useState, useEffect } from "react";
const {useState, useEffect} = React;
//import ReactDOM from "react-dom";
function App() {
const [startSeconds, setStartSeconds] = useState(Math.random());
const [progress, setProgress] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setStartSeconds(Math.random());
}, 1000);
return () => clearInterval(interval);
}, []);
useEffect(
() => {
let raf = null;
const onFrame = () => {
const currentProgress = startSeconds / 120.0;
setProgress(Math.random());
// console.log(currentProgress);
loopRaf();
if (currentProgress > 100) {
stopRaf();
}
};
const loopRaf = () => {
raf = window.requestAnimationFrame(onFrame);
// console.log('Assigned Raf ID: ', raf);
};
const stopRaf = () => {
console.log("stopped", raf);
window.cancelAnimationFrame(raf);
};
loopRaf();
return () => {
console.log("Cleaned Raf ID: ", raf);
// console.log('init', raf);
// setTimeout(() => console.log("500ms later", raf), 500);
// setTimeout(()=> console.log('5s later', raf), 5000);
stopRaf();
};
},
[startSeconds]
);
let t = [];
for (let i = 0; i < 1000; i++) {
t.push(i);
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<text>{progress}</text>
{t.map(e => (
<span>{progress}</span>
))}
</div>
);
}
ReactDOM.render(<App />,
document.querySelector("#root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
One thing that's not clear in the above answers is the order in which the effects run when you have multiple components in the mix. We've been doing work that involves coordination between a parent and it's children via useContext so the order matters more to us. useLayoutEffect and useEffect work in different ways in this regard.
useEffect runs the clean up and the new effect before moving to the next component (depth first) and doing the same.
useLayoutEffect runs the clean ups of each component (depth first), then runs the new effects of all components (depth first).
render parent
render a
render b
layout cleanup a
layout cleanup b
layout cleanup parent
layout effect a
layout effect b
layout effect parent
effect cleanup a
effect a
effect cleanup b
effect b
effect cleanup parent
effect parent
const Test = (props) => {
const [s, setS] = useState(1)
console.log(`render ${props.name}`)
useEffect(() => {
const name = props.name
console.log(`effect ${props.name}`)
return () => console.log(`effect cleanup ${name}`)
})
useLayoutEffect(() => {
const name = props.name
console.log(`layout effect ${props.name}`)
return () => console.log(`layout cleanup ${name}`)
})
return (
<>
<button onClick={() => setS(s+1)}>update {s}</button>
<Child name="a" />
<Child name="b" />
</>
)
}
const Child = (props) => {
console.log(`render ${props.name}`)
useEffect(() => {
const name = props.name
console.log(`effect ${props.name}`)
return () => console.log(`effect cleanup ${name}`)
})
useLayoutEffect(() => {
const name = props.name
console.log(`layout effect ${props.name}`)
return () => console.log(`layout cleanup ${name}`)
})
return <></>
}
Put these three lines of code in a component and you'll see their order of priority.
useEffect(() => {
console.log('useEffect')
return () => {
console.log('useEffect cleanup')
}
})
window.requestAnimationFrame(() => console.log('requestAnimationFrame'))
useLayoutEffect(() => {
console.log('useLayoutEffect')
return () => {
console.log('useLayoutEffect cleanup')
}
})
useLayoutEffect > requestAnimationFrame > useEffect
The problem you're experiencing is caused by loopRaf requesting another animation frame before the cleanup function for useEffect is executed.
Further testing has shown that useLayoutEffect is always called before requestAnimationFrame and that its cleanup function is called before the next execution preventing overlaps.
Change useEffect to useLayoutEffect and it should solve your
problem.
useEffect and useLayoutEffect are called in the order they appear in your code for like types just like useState calls.
You can see this by running the following lines:
useEffect(() => {
console.log('useEffect-1')
})
useEffect(() => {
console.log('useEffect-2')
})
useLayoutEffect(() => {
console.log('useLayoutEffect-1')
})
useLayoutEffect(() => {
console.log('useLayoutEffect-2')
})
There are two different hooks that you would need to set your eyes on when working with hooks and trying to implement lifecycle functionalities.
As per the docs:
useEffect runs after react renders your component and ensures that
your effect callback does not block browser painting. This differs
from the behavior in class components where componentDidMount and
componentDidUpdate run synchronously after rendering.
and hence using requestAnimationFrame in these lifecycles works seemlessly but has a slight glitch with useEffect. And thus useEffect should to be used to when the changes that you have to make do not block visual updates like making API calls that lead to a change in DOM after a response is received.
Another hook that is less popular but is extremely handy when dealing with visual DOM updates is useLayoutEffect. As per the docs
The signature is identical to useEffect, but it fires synchronously
after all DOM mutations. Use this to read layout from the DOM and
synchronously re-render. Updates scheduled inside useLayoutEffect will
be flushed synchronously, before the browser has a chance to paint.
So, if your effect is mutating the DOM (via a DOM node ref) and the DOM mutation will change the appearance of the DOM node between the time that it is rendered and your effect mutates it, then you don’t want to use useEffect. You’ll want to use useLayoutEffect. Otherwise the user could see a flicker when your DOM mutations take effect which is exactly the case with requestAnimationFrame
//import React, { useState, useEffect } from "react";
const {useState, useLayoutEffect} = React;
//import ReactDOM from "react-dom";
function App() {
const [startSeconds, setStartSeconds] = useState("");
const [progress, setProgress] = useState(0);
useLayoutEffect(() => {
setStartSeconds(Math.random());
const interval = setInterval(() => {
setStartSeconds(Math.random());
}, 1000);
return () => clearInterval(interval);
}, []);
useLayoutEffect(
() => {
let raf = null;
const onFrame = () => {
const currentProgress = startSeconds / 120.0;
setProgress(Math.random());
// console.log(currentProgress);
loopRaf();
if (currentProgress > 100) {
stopRaf();
}
};
const loopRaf = () => {
raf = window.requestAnimationFrame(onFrame);
// console.log('Assigned Raf ID: ', raf);
};
const stopRaf = () => {
console.log("stopped", raf);
window.cancelAnimationFrame(raf);
};
loopRaf();
return () => {
console.log("Cleaned Raf ID: ", raf);
// console.log('init', raf);
// setTimeout(() => console.log("500ms later", raf), 500);
// setTimeout(()=> console.log('5s later', raf), 5000);
stopRaf();
};
},
[startSeconds]
);
let t = [];
for (let i = 0; i < 1000; i++) {
t.push(i);
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<text>{progress}</text>
{t.map(e => (
<span>{progress}</span>
))}
</div>
);
}
ReactDOM.render(<App />,
document.querySelector("#root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Categories