setState causes infinite loop even though it is outside render function - javascript

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>
);
};

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

React component rendered from object does not get unmounted

I have the following code, where I need to run clean-up when unmounting each component step. I've set a useEffect on each Step to check if the component has been unmounted. When the parent gets a new currentStep it swaps the currently active component but the clean-up never runs. I'm wondering if this has to do with the nature of the component being rendered from an object
const Step1 = () => {
useEffect(() => {
console.log("doing things here");
return () => {
console.log("clean-up should happen here but this won't print")
}
}, []}
}
const StepMap = {
step1: <Step1/>
step2: <Step2/>
step3: <Step3/>
}
const Parent = ({ currentStep }) => {
return (
<div>
{ StepMap[currentStep] }
</div>
)
}
Alternatively this piece of code does run the clean-up, but I do find the former cleaner
const Parent = ({ currentStep }) => {
return (
<div>
{ currentStep === "step1" && StepMap[currentStep]}
{ currentStep === "step2" && StepMap[currentStep]}
</div>
)
}
Why does the first approach not work? is there a way to make it work like the second while keeping a cleaner implementation?
if you want to write javascript inside jsx we have write it inside {} curly braces like this:
import React, { useEffect, useState } from "react";
const Step1 = () => {
useEffect(() => {
console.log("Step1 doing things here");
return () => {
console.log("Step1 clean-up should happen here but this won't print");
};
}, []);
return <div>stepOne</div>;
};
const Step2 = () => {
useEffect(() => {
console.log("Step2 doing things here");
return () => {
console.log("Step2 clean-up should happen here but this won't print");
};
}, []);
return <div>steptw0</div>;
};
const Step3 = () => {
useEffect(() => {
console.log("Step3 doing things here");
return () => {
console.log("Step3 clean-up should happen here but this won't print");
};
}, []);
return <div>stepthree</div>;
};
export const StepMap = {
step1: <Step1 />,
step2: <Step2 />,
step3: <Step3 />,
};
export const Parent = ({ currentStep }) => {
return <div>{StepMap[currentStep]}</div>;
};
const App = () => {
const [steps, setSteps] = React.useState("step1");
React.useEffect(() => {
setTimeout(() => setSteps("step2"), 5000);
setTimeout(() => setSteps("step3"), 15000);
}, []);
return <Parent currentStep={steps} />;
};
export default App;

React: setInterval Not working after one interval

My Goal:
I'm trying to build a component that when you give it props.items and props.fadeEvery, it will act as a text rotator. I eventually want it to fade in an out, but I'm having trouble with my window.setInterval.
Possible Issue:
I'm calling setIndex in the useEffect hook, but is that not good practice? How an I have it iterate through the array items infinitely?
TextFade.tsx
// Imports: Dependencies
import React, { useState, useEffect } from 'react';
// TypeScript Type: Props
interface Props {
items: Array<string>,
fadeEvery: number,
};
// Component: Text Fade
const TextFade: React.FC<Props> = (props): JSX.Element => {
// React Hooks: State
const [ index, setIndex ] = useState<number>(0);
// React Hooks: Lifecycle Methods
useEffect(() => {
const timeoutID: number = window.setInterval(() => {
// End Of Array
if (index > props.items.length) {
// Set Data
setIndex(0);
}
else {
// Set Data
setIndex(index + 1);
}
}, props.fadeEvery * 1000);
// Clear Timeout On Component Unmount
return () => window.clearTimeout(timeoutID);
}, []);
return (
<div id="text-fade-container">
<p id="text-fade-text">{props.items[index]}</p>
</div>
);
};
// Exports
export default TextFade;
Your index values are taken from initital closure and it won't update unless useEffect callback is called again. You can instead use functional way to update state
useEffect(() => {
const timeoutID: number = window.setInterval(() => {
// End Of Array
setIndex(prevIdx => {
if (prevIdx > props.items.length) {
// Set Data
return 0;
}
else {
// Set Data
return prevIdx + 1;
}
})
}, props.fadeEvery * 1000);
// Clear Timeout On Component Unmount
return () => window.clearTimeout(timeoutID);
}, []);
Below I've knocked up a snippet using the callback version of setState, this avoid the closure issue you get by using useEffect with []..
const {useState, useEffect} = React;
const TextFade = (props) => {
// React Hooks: State
const [ index, setIndex ] = useState(0);
// React Hooks: Lifecycle Methods
useEffect(() => {
const timeoutID: number = window.setInterval(() => {
// End Of Array
setIndex(index =>
index + 1 >= props.items.length
? 0
: index + 1);
}, props.fadeEvery * 1000);
// Clear Timeout On Component Unmount
return () => window.clearInterval(timeoutID);
}, []);
return (
<div id="text-fade-container">
<p id="text-fade-text">{props.items[index]}</p>
</div>
);
};
ReactDOM.render(<TextFade items={['one','two', 'three']} fadeEvery={1}/>, document.querySelector('#mount'));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="mount"></div>
As #Keith said:
index is in what's called a closure. Your use effect has been told to
only render once [].. So you need to use the callback version of
setIndex
So, your useEffect hook will be:
useEffect(() => {
const timeoutID: number = window.setInterval(() => {
// End Of Array
if (index > props.items.length) {
// Set Data
setIndex(0);
} else {
// Set Data
setIndex(index + 1);
}
}, props.fadeEvery * 1000);
// Clear Timeout On Component Unmount
return () => window.clearTimeout(timeoutID);
}, [index]);
Here is the working demo at CodeSandbox.

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

Update state from axios call in useEffect hook

I'm new to react and I've just started learning about hooks and context.
I am getting some data from an API with the following code:
const getScreen = async uuid => {
const res = await axios.get(
`${url}/api/screen/${uuid}`
);
dispatch({
type: GET_SCREEN,
payload: res.data
});
};
Which goes on to use a reducer.
case GET_SCREEN:
return {
...state,
screen: action.payload,
};
In Screen.js, I am calling getScreen and sending the UUID to show the exact screen. Works great. The issue I am having is when I am trying to fetch the API (every 3 seconds for testing) and update the state of nodeupdated based on what it retrieves from the API. The issue is, screen.data is always undefined (due to it being asynchronous?)
import React, {
useState,
useEffect,
useContext,
} from 'react';
import SignageContext from '../../context/signage/signageContext';
const Screen = ({ match }) => {
const signageContext = useContext(SignageContext);
const { getScreen, screen } = signageContext;
const [nodeupdated, setNodeupdated] = useState('null');
const foo = async () => {
getScreen(match.params.id);
setTimeout(foo, 3000);
};
useEffect(() => {
foo();
setNodeupdated(screen.data)
}, []);
If I remove the [] is does actually get the data from the api ... but in an infinate loop.
The thing is this seemed to work perfectly before I converted it to hooks:
componentDidMount() {
// Get screen from UUID in url
this.props.getScreen(this.props.match.params.id);
// Get screen every 30.5 seconds
setInterval(() => {
this.props.getScreen(this.props.match.params.id);
this.setState({
nodeUpdated: this.props.screen.data.changed
});
}, 3000);
}
Use a custom hook like useInterval
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
Then in your component
useInterval(() => {
setCount(count + 1);
}, delay);
Dan Abramov has a great blog post about this
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
You can use some thing like this. Just replace the console.log with your API request.
useEffect(() => {
const interval = setInterval(() => {
console.log("Making request");
}, 3000);
return () => clearInterval(interval);
}, []);
Alternatively, Replace foo, useEffect and add requestId
const [requestId, setRequestId] = useState(0);
const foo = async () => {
getScreen(match.params.id);
setTimeout(setRequestId(requestId+1), 3000);
};
useEffect(() => {
foo();
setNodeupdated(screen.data)
}, [requestId]);

Categories