How can I stop this recurstion function - javascript

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.

Related

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

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

Stopwatch in react and useInterval

I am trying to create a stop watch using react and set interval but do not understand why count variable is always 0 is it being reset to default state. I thought since state takes some time to update hence count was always 0 but even if i increase the set interval timer it shows the same value.
Through i am trying to understand how react hooks work if someone can shed some light on functioning of hooks or redirect me to necessary links please do so
code is working in case i replace setCount(count+1) to setCount(prevCount=>prevCount+1) also you need to declare intervalId outside of app function
import "./styles.css";
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
let intervalId = -1;
const increment = () => {
console.log(count);
setCount(count + 1);
};
const handleStart = () => {
if (intervalId === -1)
intervalId = setInterval(() => {
// console.log("called");
increment();
}, 1000);
};
const handleStop = () => {
clearInterval(intervalId);
setCount(0);
};
const handlePause = () => {
clearInterval(intervalId);
};
const handleResume = () => {
handleStart();
};
return (
<div className="App">
<div className="counter">{count}</div>
<button onClick={handleStart} className="counter">
start
</button>
<button onClick={handleStop} className="counter">
stop
</button>
<button onClick={handlePause} className="counter">
pause
</button>
<button onClick={handleResume} className="counter">
resume
</button>
</div>
);
}
I'm by no means a React expert, but I watched this talk before and found it very enlightening, and I recommend you watch it too.
I also recommend you watch the first part, but the timestamp I linked is more relevant to your question.
Now in the official react docs here, they say:
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.
So, as you noticed with prevCount, it works when you do it that.
In this blog post, the writer explains that:
if you increment the count value as follows setCount(count + 1)
The the count will stuck at 0 + 1 = 1 because the variable count value when setInterval() is called is 0.
So we know that we need the previous state. If you follow the blog post, he does set up a working counter.
The way the code is right now will create an interval for each click since the intervalID gets set to -1 with each update. So, similar to the blogpost, we can have the interval ID as a state. Here's your stopwatch using that approach:
CodeSandbox
import "./styles.css";
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [intervalID, setIntervalID] = useState(0);
const handleStart = () => {
if (!intervalID) {
let interval = setInterval(() => setCount(prevCount => prevCount + 1), 1000);
setIntervalID(interval)
}
};
const handlePause = () => {
if (intervalID){
clearInterval(intervalID)
setIntervalID(0)
}
};
const handleStop = () => {
handlePause();
setCount(0)
};
const handleResume = () => {
handleStart();
};
return (
<div className="App">
<div className="counter">{count}</div>
<button onClick={handleStart} className="counter">
start
</button>
<button onClick={handleStop} className="counter">
stop
</button>
<button onClick={handlePause} className="counter">
pause
</button>
<button onClick={handleResume} className="counter">
resume
</button>
</div>
);
}
But this way, we have no way to clear the interval after unmounting the component. So we'll need useEffect(). And since we're using useEffect(), (check why here). And now, instead of saving our intervalID as the state, we can instead have an isRunning state and have useEffect create or clear the interval every time isRunning changes. So now all our handlers have to do is setIsRunning and useEffect will handle the rest.
So the code will look like this:
CodeSandbox
import "./styles.css";
import { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [isRunning, setIsRunning] = useState(0);
useEffect(()=> {
let intervalId;
if (isRunning) {
intervalId = setInterval(() => setCount(prevCount => prevCount + 1), 1000);
} else {
clearInterval(intervalId)
}
return () => clearInterval(intervalId) // Clear after unmounting
}, [isRunning])
const handleStart = () => {
setIsRunning(true);
};
const handleStop = () => {
setIsRunning(false);
setCount(0)
};
const handlePause = () => {
setIsRunning(false);
};
const handleResume = () => {
handleStart();
};
return (
<div className="App">
<div className="counter">{count}</div>
<button onClick={handleStart} className="counter">
start
</button>
<button onClick={handleStop} className="counter">
stop
</button>
<button onClick={handlePause} className="counter">
pause
</button>
<button onClick={handleResume} className="counter">
resume
</button>
</div>
);
}
I really recommend you watch the talk I linked in the beginning. I'm sure you'll find it very helpful in terms of using hooks and some of the issues you can face and how to fix them.

clearInterval not working in React Application using functional component

I wanted to build a timer application in React using functional component and below are the requirements.
The component will display a number initialized to 0 know as counter.
The component will display a Start button below the counter number.
On clicking the Start button the counter will start running. This means the counter number will start incrementing by 1 for every one second.
When the counter is running(incrementing), the Start button will become the Pause button.
On clicking the Pause button, the counter will preserve its value (number) but stops running(incrementing).
The component will also display a Reset button.
On clicking the Reset button, the counter will go to its initial value(which is 0 in our case) and stops running(incrementing).
Below is the code that I have implemented, but clearInterval doesn't seems to be working, Also how do i implement Reset Button?
Code:
import React, { useState, useEffect } from "react";
export default function Counter() {
const [counter, setCounter] = useState(0);
const [flag, setFlag] = useState(false);
const [isClicked, setClicked] = useState(false);
var myInterval;
function incrementCounter() {
setClicked(!isClicked);
if (flag) {
myInterval = setInterval(
() => setCounter((counter) => counter + 1),
1000
);
setFlag(false);
} else {
console.log("sasdsad");
clearInterval(myInterval);
}
}
function resetCounter() {
clearInterval(myInterval);
setCounter(0);
}
useEffect(() => {
setFlag(true);
}, []);
return (
<div>
<p>{counter}</p>
<button onClick={incrementCounter}>
{isClicked ? "Pause" : "Start"}
</button>
<button onClick={resetCounter}>Reset</button>
</div>
);
}
Codesandbox link:
CodeSandbox
I did a slightly different version that use an extra useEffect that runs on isRunning (changed name from flag) change:
import React, { useState, useEffect, useRef } from "react";
export default function Counter() {
const [counter, setCounter] = useState(0);
// Change initial value to `false` if you don't want
// to have timer running on load
// Changed `flag` name to more significant name
const [isRunning, setIsRunning] = useState(false);
// You don't need 2 variable for this
//const [isClicked, setClicked] = useState(false);
// Using `useRef` to store a reference to the interval
const myInterval = useRef();
useEffect(() => {
// You had this line to start timer on load
// but you can just set the initial state to `true`
//setFlag(true);
// Clear time on component dismount
return () => clearInterval(myInterval.current);
}, []);
// useEffect that start/stop interval on flag change
useEffect(() => {
if (isRunning) {
myInterval.current = setInterval(
() => setCounter((counter) => counter + 1),
1000
);
} else {
clearInterval(myInterval.current);
myInterval.current = null;
}
}, [isRunning]);
// Now on click you only change the flag
function toggleTimer() {
setIsRunning((isRunning) => !isRunning);
}
function resetCounter() {
clearInterval(myInterval.current);
myInterval.current = null;
setCounter(0);
setIsRunning(false);
}
return (
<div>
<p>{counter}</p>
<button onClick={toggleTimer}>{isRunning ? "Pause" : "Start"}</button>
<button onClick={resetCounter}>Reset</button>
</div>
);
}
Demo: https://codesandbox.io/s/dank-night-wwxqz3?file=/src/Counter.js
As a little extra i've made a version that uses a custom hook useTimer. In this way the component code is way cleaner:
https://codesandbox.io/s/agitated-curie-nkjf62?file=/src/Counter.js
Use useRef to make the interval as a ref. Then use resetCounter() to clean the interval ref.
const intervalRef = useRef(null)
const incrementCounter = () => {
intervalRef.current = setInterval(() => {
setCounter(prevState => prevState + 1)
}, 1000);
};
const resetCounter = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};
Between each rendering your variable myInterval value doesn't survive. That's why you need to use the [useRef][1] hook that save the reference of this variable across each rendering.
Besides, you don't need an flag function, as you have all information with the myClicked variable
Here is a modification of your code with those modifications. Don't hesitate if you have any question.
import React, { useState, useEffect, useRef } from "react";
export default function Counter() {
const [counter, setCounter] = useState(0);
const [isStarted, setIsStarted] = useState(false);
const myInterval = useRef();
function start() {
setIsStarted(true);
myInterval.current = setInterval(() => setCounter((counter) => counter + 1), 100);
100;
}
function pause() {
setIsStarted(false);
clearInterval(myInterval.current);
}
function resetCounter() {
clearInterval(myInterval.current);
setCounter(0);
}
return (
<div>
<p>{counter}</p>
{!isStarted ?
<button onClick={start}>
Start
</button>
:
<button onClick={pause}>
Pause
</button>
}
<button onClick={resetCounter}>Reset</button>
</div>
);
}
\\\
[1]: https://reactjs.org/docs/hooks-reference.html#useref
I'll just leave this here for anyone having the same problem.
in my case, the issue was node setInterval was used instead of window.setInterval.
this is a problem since this returns a type of Node.Timer which is an object instead of number (setInterval ID) for the clearInterval() to work as it needs an argument type of number. so to fix this,
React.useEffect(() => {
let timeoutId;
timeoutId = window.setInterval(callback, 100);
return = () => {
if(timeoutId) clearInterval(timeoutId)
}
}, [])
or in class components use componentWillMount()
You have to store myInterval in state. After that when button is clicked and flag is false, you can clear interval (myInterval in state).

How to stop restarting for loop when dependency change in useEffect (React js)

I am trying to match pi value from input.
"pracInput" dependency has been used in useEffect so I can get latest value from input and check.
But the problem is that when I input some value the for loop restart.
if I input 9;
expected value=14159; counting: 5; progress width : 60 ;
if I input another value 2;
expected => value=141592; counting: 6; progress width : 72;
import React, { useEffect, useState } from "react";
const PiGame = () => {
const [pracInput, setPracInput] = useState("1415");
const pi = "141592653589793238462643";
const [widthText, setWidthText] = useState(0);
const [counting, setCounting] = useState(0);
useEffect(() => {
const runLoop2 = async () => {
for (let i = 0; i < pracInput.length; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
if (pracInput[i] === pi[i]) {
console.log(i)
console.log(true);
setWidthText((prev) => prev + 12);
setCounting((prev) => prev + 1);
} else {
console.log(false);
alert('not match')
}
}
};
runLoop2();
}, [pracInput]);
const handleChange = (e) => {
const val = e.target.value;
if (/^[0-9]+$/.test(val)) {
setPracInput(val);
}
};
return (
<div>
<div>
value: <input
type="text"
style={{
width: "80%",
marginTop: 100,
height: 25,
fontSize:25
}}
value={pracInput}
onChange={handleChange}
/>
<div style={{ fontSize: 25 }}>counting : {counting}</div>
<div style={{ backgroundColor: "green", width: widthText, height: 20 }}></div>
<div>progress width : {widthText}</div>
</div>
</div>
);
};
export default PiGame;
According to what I understood from question:
import React, { useEffect, useState } from "react";
const PiGame = () => {
const [pracInput, setPracInput] = useState("1415");
const pi = "141592653589793238462643";
useEffect(() => {
let subscribed = true;
const runLoop2 = async () => {
for (let i = 0; i < pracInput.length; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
if (subscribed) {
if (pracInput[i] === pi[i]) {
console.log(i)
console.log(true);
} else {
console.log(false);
}
}
}
};
runLoop2();
return () => subscribed = false; // to avoid memory leak on unmount of component
}, []);. // Passing empty array will make it execute only once, at component mount
const handleChange = (e) => {
const val = e.target.value;
if (/^[0-9]+$/.test(val)) {
setPracInput(val);
}
};
return (
<div>
<div>
<input
type="text"
style={{
width: "80%",
}}
value={pracInput}
onChange={handleChange}
/>
<div>{pracInput}</div>
</div>
</div>
);
};
export default PiGame;
As you are doing asynchronous task in useEffect you must use some logic/trick to avoid memory leak.
Because you added "pracInput" in the array. "UseEffect" excutes everytime you call it and therefore will always call the looping function to start over. To stop this, you could either remove the "pracInput" from the dependency array and get the value in another way or you could use the "UseState" hook to set a certain value when the loop starts and then base a condition on your loop function call.
Something like this
const [cond, setCond] = useState(false);
Set its value to be true when the loop starts then add a condition like this
if(cond == false)
runLoop2();
Are you looking for this?
import React, { useEffect, useState, useRef } from "react";
const PiGame = () => {
const [pracInput, setPracInput] = useState("1415");
const pi = "141592653589793238462643";
const counter = useRef(4); // store the counter
useEffect(() => {
const runLoop2 = async () => {
for (let i = counter.current; i < pracInput.length; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
if (pracInput[i] === pi[i]) {
console.log(i)
console.log(true);
} else {
console.log(false);
}
}
counter.current = counter.current + pracInput.length;
};
runLoop2();
}, [pracInput]);
// the rest same as before
See your for loop is present inside the useEffect which gets triggered whenever your pracInput gets changed,so everything inside that useEffect will be triggered.Your pracInput is being changed when handleChange is being called ,so that means whenevr some input will be provided and if (/^[0-9]+$/.test(val)) { setPracInput(val); } becomes true,your pracInput will change and useEffect will be triggered and hence that for loop will start.And since your for loop requires your changes of pracInput ,moving it outside of the useEffect also wont make sense.
If you want to just start your for loop once only then remove it from that useEffect and do it like this:
useEffect(()=>{
for loop goes here
},[]);
This will ensure that for loop runs just once ,that is when the component will render for the first time.
Edit: According to your output i think this should work:
const [widthText, setWidthText] = useState(0);
const [counting, setCounting] = useState(0);
useEffect(() => {
const TimeoutId = setTimeout(() => {
if(pracInput==="1415")
{
setCounting(4);
setWidthText(60);
}
else
{
setCounting(pracInput.length+1);
setWidthText(60+(pracInput.length-4)*12);
}
}, 1000);
return () => clearTimeout(TimeoutId);
}, [pracInput]);
const handleChange = (e) => {
const val = e.target.value;
if (/^[0-9]+$/.test(val)) {
setPracInput(val);
}
};
And you can now remove the for loop

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