react-native state is read-only - javascript

Following Component in react-native:
import { useEffect, useState } from 'react'
let startValue = null // only using this to restart the counter from resetTimer() (other better approaches?)
export const NewTimer = () => {
const [seconds, setSeconds] = useState(startValue)
const formatedTime = () => {
return [pad(parseInt(seconds / 60)), pad(seconds % 60)].join(':')
}
useEffect(() => {
const timer = setInterval(() => setSeconds(++seconds), 1000) // I guess this line triggers the error
return () => {
clearInterval(timer)
}
}, [])
return formatedTime
}
const pad = (num) => {
return num.toString().length > 1 ? num : `0${num}`
}
export const resetTimer = () => {
startValue = 0
}
results in Uncaught Error: "seconds" is read-only
Can anyone point where the mistake is? Thx!

when you do ++seconds, you are attempting to mutate seconds this render, which isn't allowed. I would use the setState callback to get the current value, and just do seconds + 1, which will accomplish the same thing:
useEffect(() => {
const timer = setInterval(() => setSeconds((seconds) => seconds + 1), 1000)
return () => {
clearInterval(timer)
}
}, [])

Related

SetInterval on mount for a set duration

I have gone through some Q&As here but havent been able to understand what I am doing wrong. The following component prints 0s in console and does not update the DOM as expected.
const NotifPopup = ({ notif, index, closeHandler }) => {
const [timer, setTimer] = useState(0);
useEffect(() => {
const timerRef = setInterval(() => {
if (timer === 3) {
clearInterval(timerRef);
closeHandler(index);
} else {
console.log("timer", timer);
setTimer(timer + 1);
}
}, 1000);
}, []); // only run on mount
return (<div className="notifPopup">
<span className=""></span>
<p>{notif.message}</p>
<span className="absolute bottom-2 right-8 text-xs text-oldLace">{`closing in ${timer}s`}</span>
</div>);
};
Why is the setInterval printing a stream of 0s in console and not updating the DOM?
You are loggin, comparing, and setting a stale value due to closures.
See more use cases in a related question.
useEffect(() => {
// timerRef from useRef
timerRef.current = setInterval(() => {
setTimer((prevTimer) => prevTimer + 1);
}, 1000);
}, []);
useEffect(() => {
console.log("timer", timer);
if (timer === 3) {
clearInterval(timerRef.current);
}
}, [timer]);
Check out the code for useInterval in react-use. Inspecting the different hooks in this package can greatly improve your hooks understanding.
import { useEffect, useRef } from 'react';
const useInterval = (callback: Function, delay?: number | null) => {
const savedCallback = useRef<Function>(() => {});
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
if (delay !== null) {
const interval = setInterval(() => savedCallback.current(), delay || 0);
return () => clearInterval(interval);
}
return undefined;
}, [delay]);
};
export default useInterval;
And the usage as described in the docs:
import * as React from 'react';
import {useInterval} from 'react-use';
const Demo = () => {
const [count, setCount] = React.useState(0);
const [delay, setDelay] = React.useState(1000);
const [isRunning, toggleIsRunning] = useBoolean(true);
useInterval(
() => {
setCount(count + 1);
},
isRunning ? delay : null
);
return (
<div>
<div>
delay: <input value={delay} onChange={event => setDelay(Number(event.target.value))} />
</div>
<h1>count: {count}</h1>
<div>
<button onClick={toggleIsRunning}>{isRunning ? 'stop' : 'start'}</button>
</div>
</div>
);
};
To start the interval on mount simply change the value of isRunning on mount:
useMount(()=>{
toggleIsRunning(true);
});

React native, call a function inside setInterval when time ==60

I created a function, when user click start button that function and timer will start. However, when time reach 60 I want to call stop function but can't figure out how to that. can someone tell me how to do that please.
const [time,setTime] = useState(0)
const timeout = useRef()
const onStart = () => {
timeout.current = setInterval(() => {
if (time != 60) {
setTime(prevState => prevState + 1);
if (time == 60) {
onStop()
}
}
}, 1000);
}
const onStop = () => {
clearInterval(timeout.current);
}
If you don't have to use time in your JSX, then consider converting it to ref. There are certainly closure issues which will take place where the value of time state that you expect to be isn't what it will be. Your logic problem of correctly using if statements is also covered here.
const time = useRef(0)
const timeout = useRef()
const onStart = () => {
timeout.current = setInterval(() => {
if (time.current != 60) {
time.current+=1;
}
if (time.current == 60) {
onStop()
}
}, 1000);
}
const onStop = () => {
clearInterval(timeout.current);
}
And in case you need a state to be used in JSX, just make one timer as ref and time as state like so :-
const timer = useRef(0)
const [time,setTime] = useState(0);
const timeout = useRef()
const onStart = () => {
timeout.current = setInterval(() => {
if (timer.current != 60) {
setTime(prevState => prevState + 1);
timer.current+=1;
}
if (timer.current == 60) {
onStop()
}
}, 1000);
}
const onStop = () => {
clearInterval(timeout.current);
}
See this codesandbox example doing what you want :-
Here is another React way which is much less code and confusion :-
const [time, setTime] = useState(0);
const timeout = useRef();
useEffect(() => {
onStart();
}, []);
useEffect(() => {
if (time === 60) {
onStop();
}
}, [time]);
const onStart = () => {
timeout.current = setInterval(() => {
setTime((prevState) => prevState + 1);
}, 1000);
};
const onStop = () => {
clearInterval(timeout.current);
};
You are first checking if time != 60 this means when its 60 it will not enter in the statement and will not reach the second statement. I think you have to do it like this:
timeout.current = setInterval(() => {
if (time == 60) {
onStop()
}
setTime(prevState => prevState + 1);
}, 1000);
From the looks of it. Your code will never stop at 60. As the if statement if (time != 60) only runs the code if the time IS NOT 60. Then within that if statement, you've got another if statement going if time is 60 which it CAN'T be within that if statement as that code is only executed when time does not equal 60
Change
const [time,setTime] = useState(0)
const timeout = useRef()
const onStart = () => {
timeout.current = setInterval(() => {
if (time != 60) {
setTime(prevState => prevState + 1);
if (time == 60) {
onStop()
}
}
}, 1000);
}
const onStop = () => {
clearInterval(timeout.current);
}
to
const [time,setTime] = useState(0)
const timeout = useRef()
const onStart = () => {
timeout.current = setInterval(() => {
if (time != 60) {
setTime(prevState => prevState + 1);
}
if (time === 60) {
onStop();
}
}, 1000);
}
const onStop = () => {
clearInterval(timeout.current);
}

Faced problem with interval not being cleared in React

I was doing a project in React and what I wanted to do is to start the calculation of factorial of 1000 on start button and cancel the calculation on cancel button click. Thus, I used setInterval here is the whole code:
import React, { useState } from "react";
const Button = ({ title, countButton }) => {
const [result, setResult] = useState(0);
let interval;
const handleFactorial = (num) => {
let iteration = 1;
let value = 1;
interval = setInterval(function () {
value = value * iteration;
console.log(iteration++);
if (iteration === num) {
setResult(value);
console.log(result);
clearInterval(interval);
}
}, 0);
};
let cancelFactorial = () => {
clearInterval(interval);
};
return countButton ? (
<button onClick={() => handleFactorial(1000)}>{title}</button>
) : (
<button onClick={cancelFactorial}>{title}</button>
);
};
export default Button;
The problem is when I click on cancel button which is this one <button onClick={cancelFactorial}>{title}</button> but calculation keeps going. Thus I need your help
You should use a reference for that as if you log your interval value, you will notice that you re-assign its value on every render.
const Button = ({ title, countButton }) => {
const intervalRef = useRef();
const handleFactorial = (num) => {
intervalRef.current = setInterval(function () {...}, 0);
};
let cancelFactorial = () => {
clearInterval(intervalRef.current);
};
...
}
You need to use [useRef][1] to keep a reference to your interval.
// don't do that
// let interval;
// do this instead
const intervalRef = useRef();
interval.current = setInterval(function () { ... })
const cancelFactorial = () => {
clearInterval(interval.current);
};

Condition inside setInterval in functional component

I set an interval inside useEffect to update data every 33 seconds if a state variable can_update is true.
The initial value for can_pdate = true. The problem is, even if I change can_update to false (using disable_update function), in the update_groups function it still comes as true.
const [can_update, set_can_update] = useState(true);
const [groups, set_groups] = useState([]);
const intervalRef = useRef();
useEffect(() => {
update_groups();
const update_interval = setInterval(() => {
update_groups();
}, 33000);
intervalRef.current = update_interval;
return () => {
clearInterval(intervalRef.current);
};
}, [project_data.id]);
const update_groups = () => {
if (can_update) {
UI.get(`/project/${project_data.id}/controllers/`).then(
(data) => {
set_groups(data.groups);
},
(error) => {
console.log("Не удалось загрузить список групп");
},
);
}
};
const enable_update = () => {
set_can_update(true);
};
const disable_update = () => {
set_can_update(false);
};
I've tried moving condition into
setInterval: `const update_interval = setInterval(() => {
if (can_update){ update_groups()};
}
and replacing setInterval for recursive setTimeout. No changes.
I've had somewhat similar code inside a class component, and there didn't seem to be any problems like this.
You need add can_update to useEffect deps, otherwise
all values
from the component scope (such as props and state) that change over
time and that are used by the effect.
https://reactjs.org/docs/hooks-effect.html
In your case useEffect was called once, inside it every 33 seconds a function update_groups was called with scoped value can_update = true.
React.useEffect(() => {
if (can_update) {
update_groups();
const update_interval = setInterval(() => {
update_groups();
}, 33000);
intervalRef.current = update_interval;
return () => {
clearInterval(intervalRef.current);
};
}
}, [project_data.id, can_update]);
const update_groups = () => {
UI.get(`/project/${project_data.id}/controllers/`).then(
data => {
set_groups(data.groups);
},
error => {
console.log('Не удалось загрузить список групп');
},
);
};

not able to clear Interval using variable which is outside the react hook

I am trying to access clearTimerInterval in clearTimer method but getting undefined , got the waring variable from inside React Hook will be lost after each render. in below code useEffect hook called once once then how variable clearTimerInterval got undefined?
function Child(props) {
let [timerCount, setTimer] = useState(0);
var clearTimerInterval;
useEffect(() => {
clearTimerInterval = setInterval(() => {
setTimer(timerCount => {
return timerCount + 1;
});
}, 1000);
return () => {
clearInterval(clearTimerInterval);
};
}, []);
function clearTimer() {
clearInterval(clearTimerInterval);
}
return (
<div>
<div>Timer {timer}</div>
<button onClick={clearTimer}>ClearTimer</button>
</div>
);
}
export default React.memo(Child);
If you need to save variables across re-renders use useRef which in this case acts like a class instance field, also note that mutations to refs does not trigger a re-render.
This will give you the ability to clear the interval from outside of useEffect
function Child(props) {
let [timerCount, setTimer] = useState(0)
const intervalRef = useRef(null)
useEffect(() => {
intervalRef.current = setInterval(() => {
setTimer(prevState => prevState + 1)
}, 1000)
return () => clearInterval(intervalRef.current)
}, [])
function clearTimer() {
clearInterval(intervalRef.current)
intervalRef.current = null
}
return (
<div>
<div>Timer {timerCount}</div>
<button onClick={clearTimer}>ClearTimer</button>
</div>
)
}
Try defining the variable inside of the hook.
useEffect(() => {
var clearTimerInterval;
clearTimerInterval = setInterval(() => {
setTimer(timerCount => {
return timerCount + 1;
});
}, 1000);
return () => {
clearInterval(clearTimerInterval);
};
}, []);

Categories

Resources