How to clearInterval on componentWillUnmount() correctly in react? - javascript

I have a table that renders a mapped patientInfo array. When the patient.status is not empty/has value, a countdown timer starts counting down for individual patient in the array. This happens on componentDidMount(). Now I want to clearInterval on componentWillUnMount() but the countdown timer doesn’t stop.
basically all I need is the countdown to clear when the time get to 0. Lets say the timer starts counting down from 60 secs when it gets to 0, clear interval. there is no stop button or anything like that to execute. I need it clear interval automatically when time gets to 0 sec. the countdown time start for individual patient when they have value in status. hope that makes sense
PatientInfo Array
patientInfo = [
{ count: 959999, room: "1", name: 'John Nero', status: ''},
{ count: 959999, room: "2", name: 'Shawn Michael', status: ''},
{ count: 959999, room: "3", name: 'Gereth Macneil', status: ''}
]
//starts countdown when the patient.status has value which comes from user input
componentDidMount() {
this.countDownInterval = setInterval(() => {
this.setState(prevState => ({
patientInfo: prevState.patientInfo.map((patient) => {
if (patient.status !== '') {
// subtract a sec
return { ...patient, count: patient.count - 1000};
}
return patient;
})
}));
}, 1000);
}
//when the patient.count is 950999 clearInterval doesn't work
//edited after some comments but still doesn't work
componentWillUnmount() {
this.state.patientInfo.map((patient) => {
if (patient.count <= 950999) {
clearInterval(this.countDownInterval);
}
});
}
//after a few try the following seems to work but not sure if this is the correct way
componentDidMount() {
this.countDownInterval = setInterval(() => {
this.setState(prevState => ({
patientInfo: prevState.patientInfo.map((patient) => {
if (patient.status !== '') {
if (patient.count <= 950999) {
clearInterval(this.countDownInterval);
}
return { ...patient, count: patient.count - 1000 };
}
return patient;
})
}));
}, 1000);
}

I think the problem here is that you are clearing the interval from a call to setState from componentWillUnmount. If the component is going to unmount there is not more state to set. The component won't be there.

From the docs,
componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this method, such as invalidating timers, canceling network requests, or cleaning up any subscriptions that were created in componentDidMount().
And
You should not call setState() in componentWillUnmount() because the component will never be re-rendered. Once a component instance is unmounted, it will never be mounted again.
At max you can do this,
componentWillUnmount() {
clearInterval(this.countDownInterval);
}

Related

Does React rendering effects the setTimeout or setInterval function wrapped in a useRef and useRef is good way to achieve pause, stop, update question

I'm trying to achieve the slider functionality, where I want to slider to move to the next question after every three seconds [this part is working fine]
Features i'm trying to achieve:
OnMouseEnter: Timer stops, css animation stops(working)
OnMouseleave: Start the timer from remaining time left
OnClick: Change Question number to the selected items (working) and start animation
The issue is when I changed the question_number by onClick event, the next question start showing after every 1 seconds instead of 3 seconds. I think the problem is somehow with the React Re-rendering.
setSlider is simple useState has object:
const [timeSlider, setTimeSlider] = useState({
question_length: 4,
question_number: 0,
current_time: 0
});
I'm using useEffect to check the viewport width so react Re-render the components everytime, Viewport changes
I tried to store the Interval for timer in a react useRef. but still not working. Here is my code:
I use useRef, so I can Resume, Pause and updateNumber outSide where the first time setInterval started
const interval = useRef(null);
After EverySecond update the useState: Seconds
useEffect(() => {
setRemaining({
start_time: Date.now(),
stop_time: 0
});
interval.current = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
pauseSlider();
return () => clearInterval(interval);
}, []);
And when ever the seconds state changed, I make sure after every three seconds. question_number increased only when its less than 3
useEffect(() => {
if (seconds === 3) {
setSeconds(seconds => 0);
setTimeSlider(prev => prev.question_number === 3 ? { ...timeSlider,
question_number: 0
} : { ...timeSlider,
question_number: timeSlider.question_number + 1
});
}
}, [seconds]);
Below are the pause, resume and update function.
function pauseSlider() {
setSliderHover(true)
setRemaining({
start_time: 0,
stop_time: Date.now()
});
clearInterval(interval.current);
}
function resumeSlider() {
setSliderHover(false)
setRemaining(prev => ({ ...prev,
start_time: prev.stop_time,
stop_time: 0
}));
interval.current = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
}
function updateNumber(num) {
setSliderHover(false)
setSeconds(0)
setRemaining({
start_time: 0,
stop_time: 0
});
clearInterval(interval.current);
interval.current = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
setTimeSlider({ ...timeSlider,
question_number: num
});
}
The Problem could be in these functions, I tried to use setTimeout instead of setInterval same results.
Below is the picture of slider, so you have more clear idea what i'm trying to achieve.
Instead of using the JS, I used the GSAP for this here is my solution for it
useLayoutEffect(() => {
const ctx = gsap.context((self) => {
TimelineRef.current = gsap.timeline({
defaults: {
repeat: -1,
ease: "none",
}
})
.to(".timerSlier_left",
{
x: "0%",
duration: 10,
onRepeat: () => UpdateToNextQuestion(timeSlider),
}
)
}, ContainerRef); // <- Scope!
return () => ctx.revert(); // <- Cleanup!
}, []);
const [ remaining, setRemaining ] = useState(0);
useEffect(()=>{
if ( isInViewport ){
resumeSlider()
}else{
pauseSlider()
}
}, [ isInViewport ]);
const UpdateToNextQuestion = () => {
settimeSlider( prev => prev.question_number === 3 ? ({...prev, question_number: 0}) : ({...prev, question_number: prev.question_number+1}))
}
function pauseSlider(){
console.log("stop slider")
setSliderHover(true)
TimelineRef.current.pause();
}
function resumeSlider(){
setSliderHover(false)
TimelineRef.current.resume();
}
function updateNumber(num){
TimelineRef.current.restart();
settimeSlider( timeSlider => ({ ...timeSlider, question_number: num }) );
console.log("RESUMING")
setSliderHover(false)
TimelineRef.current.resume();
}
I'm handing timeout functionality from GSAP timeline.

How to update an object array every second in state?

I'm trying to track the time for each todo in my app by saving it in an array of objects. I managed to make it work, but since I'm updating the state only when I stop the timer, the component is not displaying each second passing.
What would be the simplest way to update the state each second? I've tried for a few days (mainly with setInterval()) but I still can't make it work since it's a little beyond my knowledge.
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
// todos array of objects
todos: null,
// value to hold temp text input
value: '',
};
...
}
// start&stop timer + updating counters
startTimer(todoKey) {
const newTodos = this.state.todos.map(element => {
if (element.key === todoKey) {
if (element.status === "stopped") {
return { ...element, status: 'started', startTime: new Date()};
} else {
let sessionSpent = new Date() - element.startTime;
let progress = element.goal !== null ? ((element.spent === null ? sessionSpent : element.spent + sessionSpent) * 1 / element.goal) : 1;
return { ...element, status: 'stopped', startTime: null, progress: progress, spent: element.spent === null ? sessionSpent : element.spent + sessionSpent };
}
} else { return element; }
});
this.setState({
todos: newTodos
});
}
....
After some more trials, I managed to make it work.
I will leave it here if anyone is having the same problem, but be aware: I'm just starting, so it's possible that I'm doing a surgery with a hammer.
If that's the case, please suggest a better solution. Thank you.
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
//todos array of objects
todos: null,
//value to hold temp text input
value: '',
//timer status: need to save it outside of the setInterval() loop so it doesn't stop after 1 iteration.
timerStatus: false,
//setInterval function container
interval: null,
// active timer todoKey: saving it here so I can avoid that a timer is stopped by pressing another timer's button.
activeTimer: null
};
...
}
// start&stop timer + updating counters
startTimer(todoKey) {
// first I check that there's no active timer
if (!this.state.timerStatus) {
// if no timer is active, I save the active timer's key in the state(so I can forbid activities on the timer from another timer's button)
this.setState({ activeTimer: todoKey });
// here I start the setInterval() function in a variable and save it into a variable (so later I can save it in the state)
let interval = setInterval(() => {
// looping through the array
const newTodos = this.state.todos.map(element => {
// finding the right object in the array so I can save the progress and the time spent, each second
if (element.key === todoKey) {
let sessionSpent = new Date() - element.startTime;
let progress = element.goal !== null ? ((element.spent === null ? sessionSpent : element.spent + sessionSpent) * 1 / element.goal) : 1;
return { ...element, status: 'started', startTime: new Date(), spent: element.spent + 1000, progress: progress }
} else {
return element;
}
});
this.setState({
todos: newTodos
});
}, 1000);
// setting the timerStatus as true(so I can have a trouth source outside of the loop when I need to stop it)
// saving the variable containint setInterval() in the state (so I can use clearInterval() from the else block
this.setState({ timerStatus: true, interval: interval });
console.log(this.state.timerStatus);
} else {
// checking if the pressed button is the one belonging to the active timer
if (todoKey === this.state.activeTimer) {
// saving in the state the timer's status (thus making possible for another timer to start
// clearing the setInterval() function
this.setState({ timerStatus: false, interval: clearInterval(this.state.interval) });
// looping through the array to save the current progress
const newTodos = this.state.todos.map(element => {
if (element.key === todoKey) {
let sessionSpent = new Date() - element.startTime;
let progress = element.goal !== null ? ((element.spent === null ? sessionSpent : element.spent + sessionSpent) * 1 / element.goal) : 1;
return { ...element, status: 'stopped', startTime: null, progress: progress, spent: element.spent === null ? sessionSpent : element.spent + sessionSpent };
} else {
return element;
}
}
);
// updating the state
this.setState({
todos: newTodos
});
} else {
// showing an error Toast when user tries to start another timer if there's one already active (when the check (todoKey === this.state.activeTimer) is false)
Toast.show("A timer is already running and multitasking is bad D:", Toast.LONG);
}
}
}
Here is a simple example:
this.setInterval( () => {
let d = new Date();
let result = d.getHours() + d.getMinutes() / MINUTES_IN_HOUR;
this.setState({
timeLineTop: result
})
}, 1000);

convert epoc time into minutes-Javascript

I want to show as an output:
User was online for 12 minutes
I'm calculating the timespent by the user on the page by setting a timeinterval.
componentDidMount() {
this.interval = setInterval(() => this.setState({ time: Date.now() }), 60000);
}
render () {
<div> User online for {this.state.time} minutes</div>
}
But this gives me:
O/P:
User online for 1532887387453 minutes
Hence this conversion for converting epoch into human readable minutes.
Is this possible?
this time is in milliseconds. So, to get minutes, you should divide it by 1000 (to get seconds), and then by 60 (to get mniutes), or simply divide it by 60000. So, use code as the following:
constructor(props) {
super(props);
this.state = { time: 0 };
}
componentDidMount() {
this.interval = setInterval(() => this.setState({ time: (Date.now() / 60000) - this.state.time }), 60000);
}
render() {
return (<div>User online for {this.state.time} minutes</div>);
}
If you're using setInterval to update the time spent, why can't you simply increment the counter after each minute,
constructor() {
super();
this.state = {
timeSpent: 0, // if you want to show 1 as the minimum, set as 1 here
}
}
componentDidMount() {
this.interval = setInterval(() => this.setState({ timeSpent: ++this.state.timeSpent }), 60000);
}
render () {
<div> User online for {this.state.timeSpent} minutes</div>
}
P.S: But keeping this time calculation logic inside a component is really bad approach.

How to play a sound only at the first time false value after true

I have person data that contain dynamic boolean value. The value is generated automatically and can be true or false every time.
Webpage get the data every 5 seconds and render it. If the value on each person is false then the sound is played.
This is the code :
import React, { Component } from 'react';
import { render } from 'react-dom';
import Sound from './Mp3';
const data = [
{
id: '1',
name: 'Peter',
value: true
},
{
id: '2',
name: 'John',
value: false
}
];
class App extends Component {
state = {
results: [],
}
componentDidMount() {
this.getData()
// get data every 5 sec
setInterval(this.getData, 5000);
}
getData = () => {
// generate random value
data[0].value = Math.random() >= 0.5;
data[1].value = Math.random() >= 0.5;
// set results state to data
this.setState({ results: data });
// condition if John or Peter value is false
if (data.some(d => d.value === false)) {
var audio = new Audio(Sound);
// play a sound
audio.play();
}
}
render() {
const { results } = this.state;
return (
<div>
{results.map(item => {
return (
<div key={item.id}>
<div>
Name: {item.name}
</div>
<div>
Value: {item.value.toString()}
</div>
<br />
</div>
)
})}
</div>
);
}
}
render(<App />, document.getElementById('root'));
This is a demo
With the code above, the sound is played every time if the value on each person is false.
How to play a sound only at the first time false value after true?
I mean, if the first rendered John value is false then play a sound and if 5 seconds later John value still false then don't play a sound after the value back to true and change to false again.
Results I expect :
// first rendered
Name: Peter
Value: true
Name: John
Value: false // play a sound
// second (5 seconds later)
Name: Peter
Value: true
Name: John
Value: false // don't play a sound
// third (10 seconds later)
Name: Peter
Value: true
Name: John
Value: true // don't play a sound
// fourth (15 seconds later)
Name: Peter
Value: true
Name: John
Value: false // play a sound
...
You'll need to keep track of each users's previous value outside of the getData function inside of another object, then compare the previous value with the new value inside of the some.
state = {
results: []
}
const previousResults = {};
componentDidMount() {
this.getData()
// get data every 5 sec
setInterval(this.getData, 5000);
}
getData = () => {
// generate random value
data[0].value = Math.random() >= 0.5;
data[1].value = Math.random() >= 0.5;
// set results state to data
this.setState({ results: data });
// condition if user value is false & previous result for user was not false
if (data.some(d => (d.value === false) && (this.previousResults[d.id] !== false))) {
var audio = new Audio(Sound);
// play a sound
audio.play();
}
data.forEach((item) => {
this.previousResults[item.id] = item.value;
});
}
You could use a sort of "Circuit Breaker" which keeps track of its state, and whether it is enabled. Having tripped, it waits an amount of time before re-enabling itself.
The code below demonstrates the following
switching from true to false executes the action callback
continuing to pulse false does NOT repeat this action
switching to true and then waiting a greater amount of time than the timeout before pulsing a new false again executes the action callback.
function TFCircuitBreaker(timeout, action){
this.state = null;
this.enabled = true;
this.valueChange = function(newValue){
if(this.enabled){
if(this.state && !newValue){
action();
this.enabled = false;
setTimeout( () => this.enabled = true,timeout);
}
}
this.state = newValue;
}
}
var cb = new TFCircuitBreaker(5000, () => console.log("Play sound"));
cb.valueChange(true);
cb.valueChange(false);
cb.valueChange(true);
cb.valueChange(false);
cb.valueChange(true);
setTimeout( () => cb.valueChange(false), 6000);
Ive also updated your demo with similar code which I think does what you want: https://stackblitz.com/edit/react-kmfiij?embed=1&file=index.js

React not passing state to child via props

I am trying to pass my parent App state to a child component Chart.
App
constructor() {
super();
this.state = {
dataPoints: {
'424353': {
date: '10/10/2016',
qty: '95'
},
'535332': {
date: '10/11/2016',
qty: '98'
},
'3453432': {
date: '10/01/2017',
qty: '94'
}
}
};
this.addPoint = this.addPoint.bind(this);
}
addPoint(dataPoint) {
let dataPoints = {...this.state.dataPoints};
const timestamp = Date.now();
dataPoints[timestamp] = dataPoint;
this.setState({ dataPoints: dataPoints });
console.log('new state', this.state.dataPoints);
}
render() {
return (
<div className="app">
<Chart dataPoints={this.state.dataPoints} />
<FormControl addPoint={this.addPoint} />
</div>
);
}
Chart
composeData() {
Object
.keys(this.props.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
componentWillUpdate() {
this.composeData();
}
The addPoint method works, i.e. I can see in the React console that the new datapoint is added to the state. But it is not reflected in the Chart component. More oddly (to me) is the fact that when I've added a point, the console.log line in my addPoint method (above):
console.log('new state', this.state.dataPoints)
does not show the new data point.
In add point
addPoint(dataPoint) {
let dataPoints = {...this.state.dataPoints};
const timestamp = Date.now();
dataPoints[timestamp] = dataPoint;
this.setState({ dataPoints: dataPoints });
console.log('new state', this.state.dataPoints);
}
In the above code you do not see the updated value because setState takes time to mutate, You must log it in the setState call back
this.setState({ dataPoints: dataPoints }, function(){
console.log('new state', this.state.dataPoints);
});
And also in the Chart component you need to bind the composeData function if you are using this.props inside it like
composeData = () =>{
Object
.keys(this.props.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
However componentWillMount is only called once and hence you will want to call the composeData function from componentWillReceiveProps also like
componentWillReceiveProps(nextProps) {
this.composeData(nextProps)
}
componentWillMount() {
this.composeData(this.props.dataPoints)
}
composeData(props){
Object
.keys(props.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
Because setState is asynchronous, use this you see the updated values:
this.setState({ dataPoints: dataPoints }, () => {
console.log('new state', this.state.dataPoints);
});
Second thing is, whenever any change happen to props values, componentWillReceiveProps lifecycle method will get called, do the computation in this method once you add the new item. Like this:
componentWillReceiveProps(newProps) {
console.log('update props values', newProps);
Object
.keys(newProps.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
Check this answer for complete explanation: why setState is asynchronous.

Categories

Resources