How to clear setTimeout when the page is changed in reactjs - nextjs - javascript

I created a slideshow in the Nextjs project, But I have a bug. When the user clicks on a link and the page has changed I get an Unhandled Runtime Error and I know it because of the setTimeout function it calls a function and tries to style an element that does not exist on the new page.
How can I clear the setTimeout function after the user click the links?
Error screenshot:
My component code:
import { useEffect, useState } from "react";
import SlideContent from "./slide-content";
import SlideDots from "./slide-dots";
import SlideItem from "./slide-item";
const Slide = (props) => {
const { slides } = props;
const [slideLength, setSlideLength] = useState(slides ? slides.length : 0);
const [slideCounter, setSlideCounter] = useState(1);
const handleSlideShow = () => {
if (slideCounter < slideLength) {
document.querySelector(
`.slide-content:nth-of-type(${slideCounter})`
).style.left = "100%";
const setSlide = slideCounter + 1;
setSlideCounter(setSlide);
setTimeout(() => {
document.querySelector(
`.slide-content:nth-of-type(${setSlide})`
).style.left = 0;
}, 250);
} else {
document.querySelector(
`.slide-content:nth-of-type(${slideCounter})`
).style.left = "100%";
setSlideCounter(1);
setTimeout(() => {
document.querySelector(`.slide-content:nth-of-type(1)`).style.left = 0;
}, 250);
}
};
useEffect(() => {
if (slideLength > 0) {
setTimeout(() => {
handleSlideShow();
}, 5000);
}
}, [slideCounter, setSlideCounter]);
return (
<>
<div className="slide-button-arrow slide-next">
<span className="carousel-control-prev-icon"></span>
</div>
<div className="slide">
{slides.map((slide) => (
<SlideContent key={`slide-${slide.id}`}>
<SlideItem img={slide.img} title={slide.title} />
</SlideContent>
))}
<SlideDots activeDot={slideCounter} totalDots={slides} />
</div>
<div className="slide-button-arrow slide-prev">
<span className="carousel-control-next-icon"></span>
</div>
</>
);
};
export default Slide;
I use my slideshow component inside the home page file.

useEffect(() => {
let timer;
if (slideLength > 0) {
timer=setTimeout(() => {
handleSlideShow();
}, 5000);
}
return () => {
clearTimeout(timer);
};
}, [slideCounter, setSlideCounter]);

you should remove your timeout function when the component unmounts.
(if you're using old syntax there is componentWillUnmount() function)
when you are using hooks you can return your useEffect so it will cause the unmount function.
in your case it will be something like this:
useEffect(() => {
//define a temp for your timeout to clear it later
let myTimeout;
if (slideLength > 0) {
//assign timeout function to the variable
myTimeout = setTimeout(() => {
handleSlideShow();
}, 5000);
}
// this triggers when the component unmounts or gets re-rendered.
// you can clear the timeout here.
return () => {
clearTimeout(myTimeout);
}
}, [slideCounter, setSlideCounter]);
you should always remove your timeouts because you don't want memory leaks and performance issues. it might not give you errors but clear them all.
there is an old post i guess you can read here

Related

How do I automatically trigger a button click after 5 seconds with React?

I have an 'Accept' button which I would like to be automatically clicked after 5 seconds. I'm using React with Next.js. The button code is:
<button name="accept" className="alertButtonPrimary" onClick={()=>acceptCall()}>Accept</button>
If I can't do this, I would like to understand why, so I can improve my React and Next.js skills.
I'm guessing you want this activated 5 seconds after render, in that case, put a setTimeout inside of the useEffect hook, like so. this will call whatever is in the hook after the render is complete.
Although this isn't technically activating the button click event.
useEffect(() => {
setTimeout(() => {
acceptCall()
}, timeout);
}, [])
in that case you should use a ref like so,
const App = () => {
const ref = useRef(null);
const myfunc = () => {
console.log("I was activated 5 seconds later");
};
useEffect(() => {
setTimeout(() => {
ref.current.click();
}, 5000); //miliseconds
}, []);
return (
<button ref={ref} onClick={myfunc}>
TEST
</button>
);
};
Hopefully, this is what you are looking for.
https://codesandbox.io/s/use-ref-forked-bl7i0?file=/src/index.js
You could create a ref for the <button> and set a timeout inside of an effect hook to call the button click event after 5 seconds.
You could throw in a state hook to limit the prompt.
import React, { useEffect, useRef, useState } from "react";
const App = () => {
const buttonRef = useRef("accept-button");
const [accepted, setAccepted] = useState(false);
const acceptCall = (e) => {
alert("Accepted");
};
const fireEvent = (el, eventName) => {
const event = new Event(eventName, { bubbles: true });
el.dispatchEvent(event);
};
useEffect(() => {
if (!accepted) {
setTimeout(() => {
if (buttonRef.current instanceof Element) {
setAccepted(true);
fireEvent(buttonRef.current, "click");
}
}, 5000);
}
}, [accepted]);
return (
<div className="App">
<button
name="accept"
className="alertButtonPrimary"
ref={buttonRef}
onClick={acceptCall}
>
Accept
</button>
</div>
);
};
export default App;

How to disappear alert after 5 seconds in React JS?

import { useState } from 'react'
const Message = ({ variant, children }) => {
const [timeOut, setTimeOut] = useState(null)
setTimeout(() => {
setTimeOut(1)
}, 3000)
return (
timeOut !== 1 && <div className={`alert alert-${variant}`}>{children}</div>
)
}
Message.defaultPros = {
variant: 'info',
}
export default Message
I want to disappear this alert after 2 or 3 seconds. I used this logic it's fine and working but In my console, I'm having this warning:
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
Is it affecting my app or is it oka? You can give me a better idea to implement this logic.
You can read through the comments
import { useState, useEffect } from 'react'
const Message = ({ variant, children }) => {
const [show, setShow] = useState(true)
// On componentDidMount set the timer
useEffect(() => {
const timeId = setTimeout(() => {
// After 3 seconds set the show value to false
setShow(false)
}, 3000)
return () => {
clearTimeout(timeId)
}
}, []);
// If show is false the component will return null and stop here
if (!show) {
return null;
}
// If show is true this will be returned
return (
<div className={`alert alert-${variant}`}>
{children}
</div>
)
}
Message.defaultPros = {
variant: 'info',
}
export default Message;
This will show the alert during 3 seconds, then it will disappear :
import React, { useEffect, useState } from 'react';
const Message = ({ variant, children }) => {
// the alert is displayed by default
const [alert, setAlert] = useState(true);
useEffect(() => {
// when the component is mounted, the alert is displayed for 3 seconds
setTimeout(() => {
setAlert(false);
}, 3000);
}, []);
return (
{alert && <div className={`alert alert-${variant}`}>{children}</div>}
)
}
If you use MUI, then you can make alerts appear and disapper with fade in transitions.
Here's the code
import Alert from "#mui/material/Alert";
import AlertTitle from "#mui/material/AlertTitle";
import Fade from "#mui/material/Fade";
export default function Registration() {
const [alertVisibility, setAlertVisibility] = useState(false);
return(
<Fade
in={alertVisibility} //Write the needed condition here to make it appear
timeout={{ enter: 1000, exit: 1000 }} //Edit these two values to change the duration of transition when the element is getting appeared and disappeard
addEndListener={() => {
setTimeout(() => {
setAlertVisibility(true)
}, 2000);
}}
>
<Alert severity="success" variant="standard" className="alert">
<AlertTitle>Success</AlertTitle>
Registration Successful!
</Alert>
</Fade>
)
}
For more transitions refer this -> https://mui.com/material-ui/transitions/
import React, { useEffect, useState } from 'react';
const Message = ({ variant, children }) => {
const [alert, setAlert] = useState(true);
useEffect(() => {
const timer = setTimeout(() => {
setAlert(false);
}, 3000);
// To clear or cancel a timer, you call the clearTimeout(); method,
// passing in the timer object that you created into clearTimeout().
return () => clearTimeout(timer);
}, []);
return (
{alert && <div className={`alert alert-${variant}`}>{children}</div>}
)
}

How can I stop this recurstion function

import React, { useCallback, useState } from "react";
import { chat_chat } from "./chat_data.json"; // Length of the chat_chat array is over 10000
function App() {
const [slug, setSlug] = useState(0);
// const [stop, setStop] = useState(false);
// const stopFn = useCallback(() => {
// setStop(!stop);
// }, [stop]);
const slugAndURLImport = useCallback(() => {
let i = 0;
let stop = false;
const loopFn = () => {
setTimeout(() => {
console.log("stop", i, stop);
if (chat_chat.length < i || stop) {
return;
}
setSlug(chat_chat[i].slug);
i++;
loopFn();
}, 2000);
};
loopFn();
return () => {
stop = true;
};
}, []);
return (
<>
<div>
<button onClick={slugAndURLImport}> "Start" </button>
</div>
<div>{slug}</div>
<div>
<button
onClick={() => {
slugAndURLImport()();
}}>
Stop
</button>
</div>
</>
);
}
export default App;
Is there a way to stop the loopFn that is running?
loopFn is a function that imports data from arrays in json.
Or is there any other way than recursive function?
I want to stop the function when I press the button.
However, a new function starts and stops, and a function that is in progress does not stop.
Is there no way that javascript can stop the function in progress?
I'd like to use it in a react hooks.
You could use setInterval instead of a setTimeout in a recursive function. If you store the output of setInterval to a variable (maybe use a useRef hook) then you can call clearInterval to stop the loop.

setState causes infinite loop even though it is outside render function

I am currently developing a simple weather app using openweatherapp API. The app is developed to fetch data from two endpoints: one that returns the current weather in your city and the other one that returns the weather forecast for next 5 days. The app should also fire an event after 60 seconds that re-fetches the data. This is how I tried to architecture my solution:
In App.js I am fetching the data and then I am passing it down as props to two other components, one that handles the current weather and the other one, the weather forecast. In the CurrentWeatherForecast component I am also initiating the function that updates the state every second using hooks. When the timer reaches 60 seconds I am calling the "handleRefresh" function that I have passed down as a prop from App.js. (in App.js is where the actual update happens). The "handleRefresh" function is outside the render method of App.js and it updates a "step" variable that should then cause the component to re-render and to re-fetch the data. The issue is that upon calling setState the function causes an infinite loop which I don't understand why since the function is outside the render method. I will post my code below.
import React, { Component } from "react";
import { CurrentWeatherForecast } from "./components/CurrentWeatherForecast";
import { NextDaysWeatherForecast } from "./components/NextDaysWeatherForecast";
export class App extends Component {
constructor(props) {
super(props);
this.state = {
currentWeather: [],
nextDaysWeather: [],
step: 0,
};
}
componentDidMount() {
const { step } = this.state;
var currentWeather;
var nextDaysWeather; // step is used to indicate wether I want to fetch data or not
if (step === 0) {
fetch(
"https://api.openweathermap.org/data/2.5/weather?q=London&appid=1fc71092a81b329e8ce0e1ae88ef0fb7"
)
.then((response) => {
const contentType = response.headers.get("content-type");
if (
!contentType ||
!contentType.includes("application/json")
) {
throw new TypeError("No JSON data!");
}
return response.json();
})
.then((data) => {
currentWeather = data;
})
.catch((error) => console.error(error));
fetch(
"https://api.openweathermap.org/data/2.5/forecast?q=London&appid=1fc71092a81b329e8ce0e1ae88ef0fb7"
)
.then((response) => {
const contentType = response.headers.get("content-type");
if (
!contentType ||
!contentType.includes("application/json")
) {
throw new TypeError("No JSON data!");
}
return response.json();
})
.then((data) => {
let requiredData = data.list.slice(0, 5);
nextDaysWeather = requiredData;
})
.catch((error) => console.error(error));
let f = setTimeout(() => {
this.setState({
currentWeather: currentWeather,
nextDaysWeather: nextDaysWeather,
step: 1, // updating step to 1 after fetching the data
});
}, 1000);
}
}
handleRefresh = () => {
const { step } = this.state;
console.log(step);
this.setState({ step: 0 }); // updating the step to 0 this causes the infinite loop
};
render() {
const { currentWeather, nextDaysWeather } = this.state;
return (
<div>
<CurrentWeatherForecast
currentWeather={currentWeather}
handleRefresh={this.handleRefresh}
/>
<NextDaysWeatherForecast nextDaysWeather={nextDaysWeather} />
</div>
);
}
}
export default App;
This was in App.js Ignore the NextDaysWeatherForecast component as it is empty for now
import React, { useEffect, useState } from "react";
export const CurrentWeatherForecast = (props) => {
const { currentWeather } = props;
const [progressValue, setValue] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setValue((progressValue) =>
progressValue < 61 ? progressValue + 1 : (progressValue = 0)
);
}, 1000);
return () => clearInterval(interval);
}, []);
if (progressValue === 60) {
props.handleRefresh(); // calling the handleRefresh function passed from App.js
}
return (
<div>
<label htmlFor="file">Downloading progress:</label>
<progress id="file" value={progressValue} max="60">
{progressValue}%
</progress>
</div>
);
};
And this was the NextWeatherForecast component where I am initiating the timer and then calling the "handleRefresh" function that I have passed down as a prop.
Thanks in advance guys !
Have a look at this effect-phase and render-phase code, and try to guess what's wrong.
useEffect(() => {
const interval = setInterval(() => {
setValue((progressValue) =>
progressValue < 61 ? progressValue + 1 : (progressValue = 0)
);
}, 1000);
return () => clearInterval(interval);
}, []);
if (progressValue === 60) {
props.handleRefresh(); // calling the handleRefresh function passed from App.js
}
This one in particular smells like an overflow: a rerender-causing function called during the render phase (and we know handleRefresh to cause rerenders.
if (progressValue === 60) {
props.handleRefresh(); // calling the handleRefresh function passed from App.js
}
Now, let's look for something that is supposed to stop the overflow (that is, it tries to set progressValue to something else than 60, once its 60).
Here it is:
progressValue < 61 ? progressValue + 1 : (progressValue = 0)
Except, this fires only every 1000ms. Which means for a whole second your component is stuck in a rerender-loop. Once it is set to 60, React starts rendering like crazy and in a very short time gets past the render limit, while progressValue is still many, many milliseconds away from being set to 0.
An example solution would be to check for progressValue === 60 in another effect.
export const CurrentWeatherForecast = (props) => {
const { currentWeather } = props;
const [progressValue, setValue] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setValue(prevProgressValue => prevProgressValue === 60 ? 0 : prevProgressValue + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
useEffect(() => progressValue === 60 && props.handleRefresh(), [progressValue]);
return (
<div>
<label htmlFor="file">Downloading progress:</label>
<progress id="file" value={progressValue} max="60">
{progressValue}%
</progress>
</div>
);
};
try this:
import React, { useEffect, useState } from "react";
export const CurrentWeatherForecast = ({ currentWeather }) => {
useEffect(() => {
const interval = setInterval(() => {
props.handleRefresh();
}, 60000);
return () => clearInterval(interval);
}, []);
return (
<div>
your codes goes here...
</div>
);
};

Countdown timer using React Hooks

So the timer works. If I hard code this.state with a specific countdown number, the timer begins counting down once the page loads. I want the clock to start counting down on a button click and have a function which changes the null of the state to a randomly generated number. I am a bit new to React. I am know that useState() only sets the initial value but if I am using a click event, how do I reset useState()? I have been trying to use setCountdown(ranNum) but it crashes my app. I am sure the answer is obvious but I am just not finding it.
If I didnt provide enough code, please let me know. I didn't want to post the whole shebang.
here is my code:
import React, { useState, useEffect } from 'react';
export const Timer = ({ranNum, timerComplete}) => {
const [ countDown, setCountdown ] = useState(ranNum)
useEffect(() => {
setTimeout(() => {
countDown - 1 < 0 ? timerComplete() : setCountdown(countDown - 1)
}, 1000)
}, [countDown, timerComplete])
return ( <p >Countdown: <span>{ countDown }</span> </p> )
}
handleClick(){
let newRanNum = Math.floor(Math.random() * 20);
this.generateStateInputs(newRanNum)
let current = this.state.currentImg;
let next = ++current % images.length;
this.setState({
currentImg: next,
ranNum: newRanNum
})
}
<Timer ranNum={this.state.ranNum} timerComplete={() => this.handleComplete()} />
<Button onClick={this.handleClick} name='Generate Inputs' />
<DisplayCount name='Word Count: ' count={this.state.ranNum} />
You should store countDown in the parent component and pass it to the child component. In the parent component, you should use a variable to trigger when to start Timer.
You can try this:
import React from "react";
export default function Timer() {
const [initialTime, setInitialTime] = React.useState(0);
const [startTimer, setStartTimer] = React.useState(false);
const handleOnClick = () => {
setInitialTime(5);
setStartTimer(true);
};
React.useEffect(() => {
if (initialTime > 0) {
setTimeout(() => {
console.log("startTime, ", initialTime);
setInitialTime(initialTime - 1);
}, 1000);
}
if (initialTime === 0 && startTimer) {
console.log("done");
setStartTimer(false);
}
}, [initialTime, startTimer]);
return (
<div>
<buttononClick={handleOnClick}>
Start
</button>
<Timer initialTime={initialTime} />
</div>
);
}
const Timer = ({ initialTime }) => {
return <div>CountDown: {initialTime}</div>;
};
useState sets the initial value just like you said, but in your case I don't think you want to store the countDown in the Timer. The reason for it is that ranNum is undefined when you start the application, and gets passed down to the Timer as undefined. When Timer mounts, useEffect will be triggered with the value undefined which is something you don't want since it will trigger the setTimeout. I believe that you can store countDown in the parent of the Timer, start the timeout when the button is clicked from the parent and send the countDown value to the Timer as a prop which would make the component way easier to understand.
Here is a simple implementation using hooks and setInterval
import React, {useState, useEffect, useRef} from 'react'
import './styles.css'
const STATUS = {
STARTED: 'Started',
STOPPED: 'Stopped',
}
export default function CountdownApp() {
const [secondsRemaining, setSecondsRemaining] = useState(getRandomNum())
const [status, setStatus] = useState(STATUS.STOPPED)
const handleStart = () => {
setStatus(STATUS.STARTED)
}
const handleStop = () => {
setStatus(STATUS.STOPPED)
}
const handleRandom = () => {
setStatus(STATUS.STOPPED)
setSecondsRemaining(getRandomNum())
}
useInterval(
() => {
if (secondsRemaining > 0) {
setSecondsRemaining(secondsRemaining - 1)
} else {
setStatus(STATUS.STOPPED)
}
},
status === STATUS.STARTED ? 1000 : null,
// passing null stops the interval
)
return (
<div className="App">
<h1>React Countdown Demo</h1>
<button onClick={handleStart} type="button">
Start
</button>
<button onClick={handleStop} type="button">
Stop
</button>
<button onClick={handleRandom} type="button">
Random
</button>
<div style={{padding: 20}}>{secondsRemaining}</div>
<div>Status: {status}</div>
</div>
)
}
function getRandomNum() {
return Math.floor(Math.random() * 20)
}
// source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
function useInterval(callback, delay) {
const savedCallback = useRef()
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback
}, [callback])
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current()
}
if (delay !== null) {
let id = setInterval(tick, delay)
return () => clearInterval(id)
}
}, [delay])
}
Here is a link to a codesandbox demo: https://codesandbox.io/s/react-countdown-demo-random-c9dm8?file=/src/App.js

Categories