Refreshing a setInterval in React - javascript

This is based on the answer given
here:
I'm having trouble resetting a setInterval.
As of now the following works. I have a prop called mediaList which contains an object array of images. When changeActiveMedia is called, the object position is moved to the next one in the list.
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { activeMediaIndex: 0 };
}
componentDidMount() {
setInterval(this.changeActiveMedia.bind(this), 5000);
}
changeActiveMedia() {
const mediaListLength = this.props.mediaList.length;
let nextMediaIndex = this.state.activeMediaIndex + 1;
if(nextMediaIndex >= mediaListLength) {
nextMediaIndex = 0;
}
this.setState({ activeMediaIndex:nextMediaIndex });
}
renderSlideshow(){
const singlePhoto = this.props.mediaList[this.state.activeMediaIndex];
return(
<div>
<img src={singlePhoto.url} />
</div>
);
}
render(){
return(
<div>
{this.renderSlideshow()}
</div>
)
}
}
My problem arises here.
I have logic that can change the number of objects in the list, mediaList.
This becomes a problem because since the interval only updates once every 5000 seconds, if the nextMediaIndex within that time is 2, and then I all of a sudden update the mediaList to have only 1 item, I run into an error since mediaList[2] would not exist.
So my question is, is there a way to RESET and CLEAR the setInterval whenever this.props.mediaList is updated?

window.setInterval returns an id which identifies an Interval timer. You can use it in conjunction with clearInterval to cancel the interval.
this.interval = setInterval(...);
...
clearInterval(this.interval);
you can use componentWillReceiveProps as kind of a generic method of checking to see if mediaList has changed. for example:
componentWillReceiveProps(nextProps) {
if (nextProps.mediaList !== this.props.mediaList) {
clearInterval(this.interval);
}
}

Related

React: Tell child component to "reinitialize," even when the passed props are the same

I have a MyComponent that renders a Timer component. My current setup is like this:
MyComponent.render:
render () {
return <Timer time={this.state.time} lag={this.lag || 0} />
}
Timer:
class Timer extends Component {
constructor(props) {
super(props);
this.state = {
time: this.props.time,
};
}
startTimer = (duration) => {
if (duration > 0){
this.on = true;
let timer = duration * 1000 + this.props.lag;
var s = setInterval(() => {
this.setState({time: Math.round(timer/1000)});
timer = timer - 500;
if (timer <= 0) {
this.on = false;
clearInterval(s);
}
}, 500);
}
}
componentDidMount = () => {
this.startTimer(this.props.time);
}
render() {
return (
<div className="Timer-container">
<div className="Timer-value">{this.state.time}</div>
</div>
);
}
}
As you can see, when the Timer is initialized, it immediately starts counting down. On subsequent renders of MyComponent, I want to restart the Timer, even if the time prop doesn't change. In other words, I want it to "reinitialize" on every render. How do I achieve this?
First of all, to reset the counter, you need to store something in the state,
either the interval (so you can clear it)
or the current time (so you can set it to the initial value).
As you want to do something if the parent re-rendered (but the props didn't change), basically what you need to check is why your component updated. An answer to that would be "Trace why a React component is re-rendering"
A quick way for your example would be to check if the state has changed (not recommended):
componentDidUpdate(prevProps, prevState, snapshot) {
if( prevState === this.state ){
clearInterval( this.state.interval );
this.startTimer( this.props.time );
}
}
Another quick solution would be (if it is an option for you) to pass a shouldRerender property to the component, and then check for this property inside the component:
// -- inside MyComponent
render () {
return <Timer
time={ this.state.time }
lag={ this.lag || 0 }
shouldRerender={ {/* just an empty object */} } />;
}
// -- inside Timer
componentDidUpdate(prevProps, prevState, snapshot) {
if( prevProps.shouldRerender !== this.props.shouldRerender ){
clearInterval( this.state.interval );
this.startTimer( this.props.time );
}
}
That looks a bit "dirty" to me. A cleaner way would be to pass some state to shouldRerender, which changes on every update (e.g. just an increasing number).
However, I think the approach to check if parent rendered is not the React way. I, personally, do consider if a component renders or not an implementation detail (I don't know if that's correct to say), that is, I don't care when React decides to render, I only care for props and state (basically).
I would recommend to think about what actually is "cause and effect", what is the reason why you want to reset the timer. Probably the re-render of the parent is only the effect of some other cause, which you might be able to use for your time reset, too.
Here some different concepts that might be useful for use cases I can imagine:
not use one Time instance, but destroy and create inside parent when needed, maybe also using a key prop.
use a HOC (like withTimer) or custom hook (like useTimer), injecting a reset() function (plus create a separate TimerView component)
keep the time state in MyComponent, passing time and onChange down to the Timer component (<Timer time={ this.state.time } onChange={ time => { this.setState({ time: time }); } } />), then both MyComponent and Timer can set / reset the time.

Is componentWillUnmount When There is setInterval?

I am creating a simple React web application where a date and time is displayed, and the time is updated every millisecond with the setState function and setInterval function. The code is below:
import React from "react";
import TitleBar from "./TitleBar";
class Time extends React.Component {
constructor(props) {
super(props);
this.state = {
date: "",
time: ""
}
this.updateTime = this.updateTime.bind(this);
}
componentDidMount() {
this.interval = setInterval(() => this.updateTime, 1);
}
componentWillUnmount() {
clearInterval(this.interval);
}
updateTime() {
let dateObject = new Date();
let date = dateObject.toDateString();
let hour = dateObject.getHours().toString();
let minute = dateObject.getMinutes().toString();
let second = dateObject.getSeconds().toString();
let millisecond = dateObject.getMilliseconds().toString();
let time = hour + " : " + minute + " : " + second + " : " + millisecond;
this.setState({
date: date,
time: time
});
}
render() {
return (
<div>
<TitleBar/>
<div className="time-div">
<p className="date-txt" value={this.state.date}> {this.state.date} </p>
<p className="time-txt" value={this.state.time}> {this.state.time} </p>
</div>
</div>
);
}
}
export default Time;
My question is, why do I need a componentWillUnmount function when the setInterval function re-renders a new time every millisecond? When I remove the componentWillUnmount function, the application works perfectly as intended, so I don't understand why I should unmount the component or "remove" the setInterval function. Is this just a convention?
My question is, why do I need a componentWillUnmount function when the setInterval function re-renders a new time every millisecond?
So that when your component is unmounted (removed from the page entirely), the interval timer is stopped and the callback stops getting called.
When I remove the componentWillUnmount function, the application works perfectly as intended,
No, it will try to set state on an unmounted component, which is an error. (Though you may not see much in terms of effects of the error except that over time your page may get slower as more and more endless intervals get scheduled and repeated. If you're using the development version of the React libs in development, they'll give you an error in the console when you try to set state on an unmounted component.)
Here's your component with that line commented out with an error message when it tries to set state after being unmounted:
class Time extends React.Component {
constructor(props) {
super(props);
this.state = {
date: "",
time: ""
}
this.updateTime = this.updateTime.bind(this);
}
componentDidMount() {
this.interval = setInterval(this.updateTime, 1);
}
componentWillUnmount() {
// Removed to cause error: clearInterval(this.interval);
}
updateTime() {
let dateObject = new Date();
let date = dateObject.toDateString();
let hour = dateObject.getHours().toString();
let minute = dateObject.getMinutes().toString();
let second = dateObject.getSeconds().toString();
let millisecond = dateObject.getMilliseconds().toString();
let time = hour + " : " + minute + " : " + second + " : " + millisecond;
this.setState({
date: date,
time: time
});
}
render() {
return (
<div>
{/*Omitted for example <TitleBar />*/}
<div className="time-div">
<p className="date-txt" value={this.state.date}> {this.state.date} </p>
<p className="time-txt" value={this.state.time}> {this.state.time} </p>
</div>
</div>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
showTime: true
};
}
componentDidMount() {
this.timer = setTimeout(() => {
this.setState({showTime: false});
}, 200);
}
componentWillUnmount() {
clearTimeout(this.timer);
}
render() {
return <div>{this.state.showTime && <Time />}</div>;
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.development.js"></script>
All the SideEffects like your events which may be there (In your case setInterval and Timer associated to it is stopped)
can be Cleaned Up in componentWillUnMount() just before the Component gets removed from the DOM Tree.
So, ComponentWillUnMount() is for all the CleanUp stuff, so that we avoid Memory Leaks and improve performance of our Product.
Part B:
When I remove the componentWillUnmount function, the application works
perfectly as intended.
That doesn't happen actually, you may not experience it in small app. To cross verify open Developer Window and In the events section check whether the Timer functionality is still being fired or not.
Because if you keep setInterval after unmounting the component it will continue to run as an excess load for the browser and it can make your web application slower.

React: SetInterval in ComponentDidMount causing error "Warning: Can't perform a React state update on an unmounted component."

I want to add a typing effect to a component in my React app and I'm using setInterval to do this. Everything works fine but I'm getting the following error:
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 the componentWillUnmount method
The function starts in componentDidMount() so I don't understand why it's saying that I'm updating an unmounted component. I've tried to add clearInterval() in componentWillUnmount() but the error still shows.
Code:
componentDidMount = () => {
this.typeText();
}
componentWillUnmount(){
console.log(this.state.intervalId);
clearInterval(this.state.intervalId);
}
typeText = () => {
const sp = (text,key) => <span key={key} style={{whiteSpace: 'pre-line'}}>{text}</span>;
const results = this.state.screenText;
let start = 0;
let cursor = 0;
const intervalId = setInterval(() => {
if (results.length) results.pop();
const str = this.state.text.slice(start,cursor);
const span = sp(str,cursor);
results.push(span);
this.setState({screenText:results});
start = Math.floor((cursor / 80));
cursor += 1;
if (cursor > this.state.text.length) clearInterval(intervalId);
},5);
this.setState({intervalId: intervalId});
console.log(this.state.intervalId);
}
render() {
return <span id="typing"> {this.state.screenText}</span>
}
I think the problem with your code is that you are saving intervalId in the state of the component.
As you probably know when you call setState it causes rerender.
you can save your intervalId in the attribute of class.
please consider these changes in your code:
Define an attribute in your class like this
class MyClsss extends React.component{
intervalId = "";
...
}
Save your interval id in this attribute like this
this.intervalId = setInterval(...)
Clear your interval wherever you want this way
clearInterval(this.intervalId);

how to display in react render without rerendering, automatically changing values of mobx store instance?

how to pull in real time properties of an mobx class?
i made an instances of mobx store in which i calculate the years and days
of several planets.
each planet is an instance which uses PlanetStore function
with a setInreval which allways add days and years.
i made a list with all the instances.
now, i want to dispaly this changes of years and days in real-time in a react-mobx componenet. but, its not observable. how can i do this??
#observer
class Calendar extends Component{
render(){
let planets = planetStore.Planets *just getting names of planets not relevant
let planetArr=[]
for(let i=1; i<planets.length; i++){
let key= Object.keys(planets[i])
planetArr.push(key)
}
this.planets=planetArr
planets.forEach(e=> calanderStore.loadPlanets(e))
console.log(planetArr)
console.log(calanderStore.list)
calanderStore.showPercent()
return (
<div id='calendar'> {this.planets.map((planet, index) => <Cal planet={planet} key={index}/> )} </div> )
}
}
export default Calendar
*making a instance for each planet
class CalendarItem{
#observable currentPrecent=0
#observable planetName
constructor(planetName,currentPrecent) {
this.planetName=planetName
this.currentPrecent=currentPrecent
this.dayCounter=0
this.yearCounter=-1
}
#action makeInstnace=(planet)=>{
this.planetName=planet
let currentPrecent=this.currentPrecent
let dayCounter=this.dayCounter
let yearCounter=this.yearCounter
let planetData=new
CalendarItem(`${planet}`,`${currentPrecent}`,`${yearCounter}`,`${dayCounter}`)
return (
planetData
)
}
}
export default new CalendarItem();
#observer
class Cal extends Component{
constructor(props) {
super(props)
this.state={rerender:''}
this.planetName=this.props.planet
this.currentPrecent=0
this.dayCounter=0
this.yearCounter=-1
}
render(){
let planet=this.props.planet
**here i take the planet from the list of instances i made in CalandareStore Below**
dayCounter=JSON.parse(JSON.stringify(calanderStore.list[planetIndex].dayCounter))
console.log(dayCounter)
let yearCounter=planet.yearCounter
return(
<div className="calendaritem"> **the instance is being updated
<span>{dayCounter}</span> but it is not observed in here
<div></div>
<div>{calanderStore.list[planetIndex].yearCounter}</div>
class CalanderStore {
#observable list=[] *list of instances*
#action loadPlanets=(planetName)=>{
console.log(planetName)
planetName=JSON.parse(JSON.stringify(planetName))
planetName=Object.keys(planetName)
console.log(planetName)
let ifexist=this.findPlanetFromList(planetName)
if(ifexist.length==0){
let newPlanet=CalendarItem.makeInstnace(planetName)
return this.list.push(newPlanet)
}
}
#action showPercent= (id)=> {
console.log("Calander test now")
this.list.forEach(id=>{
id=JSON.parse(JSON.stringify(id))
let index=planetStore.findPlanetIndex(id.planetName)
let test=planetStore.Planets[index][id.planetName].animationDuration['animation-duration']
test=test.replace('s','0')
test=parseInt(test)
console.log(test)
let setTimer=(id)=>{
// id=id[0]
// planetName=id[0].planeName
// id=JSON.parse(JSON.stringify(id))
if (id.currentPrecent < 100) {
console.log(id)
id.currentPrecent += 1;
id.dayCounter=Math.round(id.currentPrecent*planetStore.Planets[index][id.planetName].daysInyear/100)
}
else {
id.currentPrecent=0
id.dayCounter=0;
id.yearCounter+=1;
console.log(id.dayCounter)
}
}
// let getanimationduration=this.getAnimeDuration
// let totalDuration = getanimationduration(planetName)
setInterval(() => {
setTimer(id)}, test);
})
}
Yes you can control whether the component should update or not there is a function of react component class
shouldComponentUpdate ( nextProps, nextState, nextContext ) {
/* compare nextState with your current states properties if you want to update on any basis return true if you want to render the component again */
return true; // will re-render component ,
return false; // do not re-render component even if you change component states properites
}
I'm not really sure what is going on with your code, but I have noticed that you are using setInterval inside the #action.
Mobx actions only apply to the current stack, code inside the setTimeout, Promise, setInterval does not run in the same stack.
Mobx has special functions for writing asynchronous code.
Writing asynchronous actions

Using setInterval in React delays component render

I have a parent component, <App>:
constructor() {
super();
this.state = {
transporterPos: 0
}
this.tick = this.tick.bind(this);
}
componentDidMount() {
this.timerId = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.timerId);
}
tick() {
let transporterPos = this.state.transporterPos;
transporterPos++;
if (transporterPos > 7) {
transporterPos = 0;
}
this.setState({ transporterPos: transporterPos });
}
render() {
return (
<div>
<Staves transporterPos={this.state.transporterPos}/>
</div>
);
}
The <Staves> component contains several <Stave> components, each of which contains several <Note> components. Each <Note> component is injected with a className conditional on its active property being true:
<div className="noteContainer" onClick={this.handleClick}>
<div className={"note" + (this.props.active ? ' active' : '')}></div>
</div>
handleClick() is a method that toggles a <Note>'s active property. I'm not including all the code here to make this more readable. The problem is that when clicking on a <Note>, although its active property changes immediately, the styling given by the conditional className of 'active' is not visible until the component is re-rendered at the next "tick" of the setInterval method. In other words, rendering only seems to happen once every 1000ms. I would like it to happen immediately. Am I using setInterval wrong?
Edit:
In response to comments, here is the handleClick method (in <Note>):
handleClick() {
this.props.toggleActive(this.props.pos);
}
This calls toggleActive in <Stave>:
toggleActive(pos) {
this.props.notes[pos].active = !this.props.notes[pos].active;
}
props.notes here is part of <App>'s state, which is passed down to <Stave> (and which I didn't include in this question for the sake of brevity).
toggleActive(pos) {
this.props.notes[pos].active = !this.props.notes[pos].active;
}
The reason a re-render isn't being triggered is because this.props is mutated directly instead of with setState. Move toggleActive further up to where you can use setState.
If necessary you can pass the function as a prop to the child component and call it via this.props.toggleActive()
Besides not triggering a re-render, another reason this.props should never be mutated directly is because your changes will get overwritten whenever the parent changes state and passes props to its children.

Categories

Resources