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);
Related
i am having this handleCheckClick funtion witch gets Data i want to store the data into a state every time the handleCheckClick funtion is called so after many times handleCheckClick is called the state should look like the object array below
handleCheckClick = (e, stateVal, index) => {
let prevState = [...this.state[stateVal]];
prevState[index].positive = e.target.checked;
console.log(index);
this.setState({ [stateVal]: prevState });
var date = moment(this.state.dateState).format("YYYY-MM-DD");
const { id, checked } = e.target.dataset;
console.log(stateVal);
if (e.target.checked) {
var checkbox = "True";
} else {
var checkbox = "False";
}
const Data = {
boolvalue: checkbox,
date: date,
userid: id,
};
this.setState({ datastate : Data });// something like this
};
after many times the handleCheckClick funtion is called the state must look like this
[
{
"date" : "2022-02-15",
"userid" : 6,
"boolvalue" : true
},
{
"date" : "2022-02-15",
"userid" : 5,
"boolvalue" : false
},
{
"date" : "2022-02-15",
"userid" :7,
"boolvalue" : true
},
{
"date" : "2022-02-15",
"userid" : 11,
"boolvalue" : true
},
{
"date" : "2022-02-15",
"id" : 4,
"boolvalue" : false
}
]
pls create a codesandbox example
https://codesandbox.io/s/recursing-wind-mjfjh4?file=/src/App.js
You have to take your data and call setState using the existing data merged with the new Data object. The merging can be done using ... (spread) operator. Here's the code with the relevant parts:
class Component extends React.Component {
handleClick = (e) => {
// build up a new data object:
if (e.target.checked) {
var checkbox = "True";
} else {
var checkbox = "False";
}
const { id } = e.target.dataset
var date = moment(this.state.dateState).format("YYYY-MM-DD");
const Data = {
boolvalue: checkbox,
date: date,
userid: id,
};
// set the new state, merging the Data with previous state (accesible via this.state)
// this creates a new array with all the objects from this.state.datastate and the new Dataobject
this.setState({
datastate: [...this.state.datastate, Data]
})
}
// log the state on each update for seeing changes.
componentDidUpdate() {
console.log('Component did update. State:', this.state)
}
// Rendering only a button for showcasing the logic.
render() {
return <button onClick={this.handleClick}></button>
}
constructor(props) {
super(props)
// initialise an empty state
this.state = {
datastate: [],
dateState: new Date()
}
}
}
Edit for removing an element when unchecked:
You can remove a certain element by its id in the onClick handler when the box is unchecked:
class Component extends React.Component {
handleClick = (e) => {
// get id first.
const { id } = e.target.dataset
// if element is not checked anymore remove its corresponding data:
if(e.target.checked === false) {
// remove the element by filtering. Accept everything with a different id!
const update = this.state.datastate.filter(d => d.userid !== id)
this.setState({
datastate: update
})
// end handler here..
return
}
// if we get here, it means checkbox is checked now, so add data!
var date = moment(this.state.dateState).format("YYYY-MM-DD");
const Data = {
// it is better to keep the boolean value for later use..
boolvalue: e.target.checked,
date: date,
userid: id,
};
// set the new state, merging the Data with previous state (accesible via this.state)
// this creates a new array with all the objects from this.state.datastate and the new Dataobject
this.setState({
datastate: [...this.state.datastate, Data]
})
}
// log the state on each update for seeing changes.
componentDidUpdate() {
console.log('Component did update. State:', this.state)
}
// Rendering only a button for showcasing the logic.
render() {
return <button onClick={this.handleClick}></button>
}
constructor(props) {
super(props)
// initialise an empty state
this.state = {
datastate: [],
dateState: new Date()
}
}
}
Feel free to leave a comment
I am building a stopwatch by using useState hooks in react.js but while implementing pause functionality I have noticed that I am not able to clear the interval. I am new and I have tried many things but still it doesn't work. If anyone can help me fix the code or suggest me other way.
Here is my code:
function App() {
const [stopwatch, setStopwatch] = useState({
hour: 0,
min: 0,
sec: 0,
secElapsed: 0,
minElapsed: 0,
});
const [buttonState, setButtonState] = useState({
start: true,
stop: false,
pause: false,
resume: false,
});
var interval = null;
function onStart() {
// i want to clear this interval when the onPause function is called
var clrInt = setInterval(() => {
setStopwatch(prevValue => {
prevValue.secElapsed++;
return {
hour: Math.floor(prevValue.secElapsed / 3600),
minElapsed: Math.floor((prevValue.secElapsed + 1) / 60),
min: prevValue.minElapsed % 60,
sec: prevValue.secElapsed % 60,
secElapsed: prevValue.secElapsed,
};
});
}, 1000);
setButtonState(prevValue => {
return {
...prevValue,
start: false,
pause: true,
stop: true,
};
});
interval = clrInt;
}
function onPause() {
setButtonState(prevValue => {
return {
...prevValue,
pause: false,
resume: true,
};
});
// i want to clear the interval in onStart function here
clearInterval(interval);
}
return (
<div>
<h1>
{stopwatch.hour < 10 ? '0' + stopwatch.hour : stopwatch.hour}:
{stopwatch.min < 10 ? '0' + stopwatch.min : stopwatch.min}:
{stopwatch.sec < 10 ? '0' + stopwatch.sec : stopwatch.sec}
</h1>
{buttonState.start ? <button onClick={onStart}>Start</button> : null}
{buttonState.pause ? <button onClick={onPause}>Pause</button> : null}
{buttonState.stop ? <button>Stop</button> : null}
{buttonState.resume ? <button>Resume</button> : null}
</div>
);
}
Declaring a variable inside the function component will run every time the component is rendered. You need to capture the interval in a useState like
var [intervalState, setIntervalState] = useState(null)
...
function onStart() {
var clearInterval = setInterval(() => {
...
})
setIntervalState(clearInterval)
}
This will make sure your clearInterval value will persist across rerenders
I don't think the interval fits into the component's state. Instead, I would use the useRef hook to keep it through the component's lifetime.
Anyways, it's good that you start thinking in hooks :)
A cleaner way is to implement something like useInterval:
const [isRunning, setIsRunning] = useState(true)
useInterval(() => {
// Your custom logic here
}, isRunning ? 1000 : null)
const onPause = () => {
// ...
setIsRunning(false)
}
Here's a demo with an actual implementation of the hook.
Please help me it always says Cannot read property 'commit' of undefined. Here is my code in store.js.
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
state:{
timer: null,
totaltime: (25 * 60)
},
mutations:{
startTimer: (state, context) => {
//here is the problem part I think
state.timer = setInterval(() => context.commit('countDown'),
1000)
},
countDown: state => {
var time = state.totaltime
if(time >= 1){
time--
}else{
time = 0
}
},
stopTimer: state => {
clearInterval(state.timer)
state.timer = null
},
resetTimer: state => {
state.totaltime = (25 * 60)
clearInterval(state.timer)
}
},
getters:{
minutes: state => {
const minutes = Math.floor(state.totaltime / 60);
return minutes;
},
seconds: (state, getters) => {
const seconds = state.totaltime - (getters.minutes * 60);
return seconds;
}
},
actions:{
}
})
I have problem it debugging. it always says like this
'Cannot read property 'commit' of undefined'
Here is my Timer.vue code for calling
methods: {
formTime(time){
return (time < 10 ? '0' : '') + time;
},
startTimer(){
this.resetButton = true
this.$store.commit('startTimer')
},
stopTimer(){
this.$store.commit('stopTimer')
},
resetTimer(){
this.$store.commit('resetTimer')
},
},
computed: {
minutes(){
var minutes = this.$store.getters.minutes;
return this.formTime(minutes)
},
seconds(){
var seconds = this.$store.getters.seconds;
return this.formTime(seconds);
},
timer: {
get(){
return this.$store.state.timer
}
}
}
My code in Timer.vue script computed and methods. I cannot track where the problem is... Please help me Im stuck with this here.
Mutations do not have access to any context. They are meant to be atomic, that is they work directly with one facet of state. You should make your startTimer an action that commits the timer and then starts the countdown
mutations: {
// add this one for setting the timer
setTimer (state, timer) {
state.timer = timer
}
},
actions: {
startTimer ({ commit }) {
commit('stopTimer') // just a guess but you might need this
commit('setTimer', setInterval(() => {
commit('countDown')
}, 1000))
}
}
This would need to be called via dispatch instead of commit
this.$store.dispatch('startTimer')
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);
}
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