Prevent re-render ReactJS when using setInterval() - javascript

Hello guys ? So i'm developing web with React right now, and i need to make stopwatch timer that will be played when i click the button, so i use setInterval() method. But when i click the button and setInterval() is running, i got this warning from why-did-you-update library, which means to avoid re-rendering. I already use PureComponent but why when setInterval() is running some component still re-render? it makes lag guys, and if i stop setInterval with clearInterval() method the component is stop re-rendering.
The code just like this:
startTimer() {
// Test
this.setState(
prevState => ({
...prevState,
isSessionStarted: true,
duration: prevState.duration,
startedAt: Date.now() - prevState.duration
}),
() => {
this.timer = setInterval(
() =>
this.setState(prevState => ({
duration: Date.now() - prevState.startedAt
})),
1000
);
}
);
}
stopTimer() {
this.setState({ isSessionFinished: true }, () => {
clearInterval(this.timer);
});
}
Any solution? Thank you before.

Related

Stop setInterval() when Component Unmounts? (React)

I am looking to stop an interval function when a user on my website changes the page. I searched online for "how to tell if a react element is visible on the screen," but I can only find results for intersection observers. Is there a way to stop an interval function when a react component dismounts? (ps. I believe dismounting is when the component is no longer rendered?)
Heres my component interval:
useEffect(() => {
const updatePostInfo =
inView &&
setInterval(() => {
dispatch(getPostInfo(data._id));
}, 3500);
!inView && clearInterval(updatePostInfo);
}, [inView]);
Currently, I have it set up with an intersection observer so that it doesn't dispatch the function when it isn't visible. But the interval still runs when the page changes?
You need to return a cleanup function from your effect:
useEffect(() => {
const updatePostInfo = setInterval(() => {
dispatch(getPostInfo(data._id));
}, 3500);
return () => clearInterval(updatePostInfo);
}, []);

SwipeableViews How to pass the updateHeight() as a prop

So I am using SwipeableViews no swipe between pages. My problem is each page have a different dynamic length. I have animateHeight as true but it only animate the height when the tab changes. In the documentation it says that the function updateHeight() can solve this.
https://react-swipeable-views.com/api/api/
Due to my lack of knowledge I just could not get the updateHeight since all the exampels I saw on class based app. I built my app in function based app. I just could not figure it out to pass it as props to call it later.
I've found a way to use the updateHeight function. It's a bit hacky, but it works 😅. I've added an interval and timeout to compensate for data loading in/slow machines. The useEffect cleans the interval when the page is unmounted.
There's probably a better solution, but this is what I've found so far!
export function SwipeView() {
const [ref, setRef] = useState(null);
const onRefChange = useCallback((node: SwipeableViews & HTMLDivElement) => {
// hacky solution to update the height after the first render. Height is not set correctly on initial render
setRef(node); // e.g. change ref state to trigger re-render
if (node === null) {
return;
} else {
interval = setInterval(() => {
// #ts-ignore typings are not correct in this package
node.updateHeight();
}, 100);
setTimeout(() => {
clearInterval(interval);
}, 10000);
}
}, []);
useEffect(() => {
return () => clearInterval(interval);
}, [interval]);
return (
<SwipeableViews
animateHeight
ref={onRefChange}
>
{children}
</SwipeableViews>
);
}

React Hooks, setTimeOut to set State to false

I have this scenario
const [showAlertLink, setShowAlertLink] = useState(false);
const handleClick = () => {
setShowAlertLink(true);
};
<Button disableRipple onClick={handleClick}>
{!!showAlertLink && <Alert icon={<CheckIcon fontSize="inherit" />} onClose={() => setShowAlertLink(false)}>{t(`alert_link_copied`)}</Alert>}
At the moment if i click on the Icon i can close the Alert.
How can i make this alert closing automatically with a SetTimeout duration 2 seconds for example?
You can use useEffect:
useEffect(() => {
let timeout
if (showAlertLink) {
timeout = setTimeout(() => setShowAlertLink(false), 2000);
}
return () => clearTimeout(timeout);
}, [showAlertLink]);
It will trigger when showAlertLink value changes. If it is true it will set a timeout to set it to false in 2 seconds (2000).
Be careful that you always return the effect to clear the timeout (to ensure you don't get tried to update state of an unmounted component) error.
If you are using typescript, you can type timeout like so:
let timeout: ReturnType<typeof setTimeout>;

Creating a stopwatch with redux

I've been trying to make a stopwatch in react and redux. I've been having trouble trouble figuring out how to design such a thing in redux.
The first thing that came to mind was having a START_TIMER action which would set the initial offset value. Right after that, I use setInterval to fire off a TICK action over and over again that calculates how much time has passed by using the offset, adds it to the current time, and then updates the offset.
This approach seems to work, but I'm not sure how I would clear the interval to stop it. Also, it seems like this design is poor and there is probably a better way to do it.
Here is a full JSFiddle that has the START_TIMER functionality working. If you just want to see what my reducer looks like right now, here it is:
const initialState = {
isOn: false,
time: 0
};
const timer = (state = initialState, action) => {
switch (action.type) {
case 'START_TIMER':
return {
...state,
isOn: true,
offset: action.offset
};
case 'STOP_TIMER':
return {
...state,
isOn: false
};
case 'TICK':
return {
...state,
time: state.time + (action.time - state.offset),
offset: action.time
};
default:
return state;
}
}
I would really appreciate any help.
I would probably recommend going about this differently: store only the state necessary to calculate the elapsed time in the store, and let components set their own interval for however often they wish to update the display.
This keeps action dispatches to a minimum — only actions to start and stop (and reset) the timer are dispatched. Remember, you're returning a new state object every time you dispatch an action, and each connected component then re-renders (even though they use optimizations to avoid too many re-renders inside the wrapped components). Furthermore, many many action dispatches can make it difficult to debug app state changes, since you have to deal with all the TICKs alongside the other actions.
Here's an example:
// Action Creators
function startTimer(baseTime = 0) {
return {
type: "START_TIMER",
baseTime: baseTime,
now: new Date().getTime()
};
}
function stopTimer() {
return {
type: "STOP_TIMER",
now: new Date().getTime()
};
}
function resetTimer() {
return {
type: "RESET_TIMER",
now: new Date().getTime()
}
}
// Reducer / Store
const initialState = {
startedAt: undefined,
stoppedAt: undefined,
baseTime: undefined
};
function reducer(state = initialState, action) {
switch (action.type) {
case "RESET_TIMER":
return {
...state,
baseTime: 0,
startedAt: state.startedAt ? action.now : undefined,
stoppedAt: state.stoppedAt ? action.now : undefined
};
case "START_TIMER":
return {
...state,
baseTime: action.baseTime,
startedAt: action.now,
stoppedAt: undefined
};
case "STOP_TIMER":
return {
...state,
stoppedAt: action.now
}
default:
return state;
}
}
const store = createStore(reducer);
Notice the action creators and reducer deals only with primitive values, and does not use any sort of interval or a TICK action type. Now a component can easily subscribe to this data and update as often as it wants:
// Helper function that takes store state
// and returns the current elapsed time
function getElapsedTime(baseTime, startedAt, stoppedAt = new Date().getTime()) {
if (!startedAt) {
return 0;
} else {
return stoppedAt - startedAt + baseTime;
}
}
class Timer extends React.Component {
componentDidMount() {
this.interval = setInterval(this.forceUpdate.bind(this), this.props.updateInterval || 33);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
const { baseTime, startedAt, stoppedAt } = this.props;
const elapsed = getElapsedTime(baseTime, startedAt, stoppedAt);
return (
<div>
<div>Time: {elapsed}</div>
<div>
<button onClick={() => this.props.startTimer(elapsed)}>Start</button>
<button onClick={() => this.props.stopTimer()}>Stop</button>
<button onClick={() => this.props.resetTimer()}>Reset</button>
</div>
</div>
);
}
}
function mapStateToProps(state) {
const { baseTime, startedAt, stoppedAt } = state;
return { baseTime, startedAt, stoppedAt };
}
Timer = ReactRedux.connect(mapStateToProps, { startTimer, stopTimer, resetTimer })(Timer);
You could even display multiple timers on the same data with a different update frequency:
class Application extends React.Component {
render() {
return (
<div>
<Timer updateInterval={33} />
<Timer updateInterval={1000} />
</div>
);
}
}
You can see a working JSBin with this implementation here: https://jsbin.com/dupeji/12/edit?js,output
If you are going to use this in a bigger app then I would use requestAnimationFrame instead of an setInterval for performance issues. As you are showing milliseconds you would notice this on mobile devices not so much on desktop browsers.
Updated JSFiddle
https://jsfiddle.net/andykenward/9y1jjsuz
You want to use the clearInterval function which takes the result of a call to setInterval (a unique identifier) and stops that interval from executing any further.
So rather than declare a setInterval within start(), instead pass it to the reducer so that it can store its ID on the state:
Pass interval to dispatcher as a member of the action object
start() {
const interval = setInterval(() => {
store.dispatch({
type: 'TICK',
time: Date.now()
});
});
store.dispatch({
type: 'START_TIMER',
offset: Date.now(),
interval
});
}
Store interval on new state within the START_TIMER action reducer
case 'START_TIMER':
return {
...state,
isOn: true,
offset: action.offset,
interval: action.interval
};
______
Rendering the component according to interval
Pass in interval as a property of the component:
const render = () => {
ReactDOM.render(
<Timer
time={store.getState().time}
isOn={store.getState().isOn}
interval={store.getState().interval}
/>,
document.getElementById('app')
);
}
We can then inspect the state within out component to render it according to whether there is a property interval or not:
render() {
return (
<div>
<h1>Time: {this.format(this.props.time)}</h1>
<button onClick={this.props.interval ? this.stop : this.start}>
{ this.props.interval ? 'Stop' : 'Start' }
</button>
</div>
);
}
______
Stopping the timer
To stop the timer we clear the interval using clearInterval and simply apply the initialState again:
case 'STOP_TIMER':
clearInterval(state.interval);
return {
...initialState
};
______
Updated JSFiddle
https://jsfiddle.net/8z16xwd2/2/
Similar to andykenward's answer, I would use requestAnimationFrame to improve performance as the frame rate of most devices is only about 60 frames per second. However, I would put as little in Redux as possible. If you just need the interval to dispatch events, you can do that all at the component level instead of in Redux. See Dan Abramov's comment in this answer.
Below is an example of a countdown Timer component that both shows a countdown clock and does something when it has expired. Inside the start, tick, or stop you can dispatch the events that you need to fire in Redux. I only mount this component when the timer should start.
class Timer extends Component {
constructor(props) {
super(props)
// here, getTimeRemaining is a helper function that returns an
// object with { total, seconds, minutes, hours, days }
this.state = { timeLeft: getTimeRemaining(props.expiresAt) }
}
// Wait until the component has mounted to start the animation frame
componentDidMount() {
this.start()
}
// Clean up by cancelling any animation frame previously scheduled
componentWillUnmount() {
this.stop()
}
start = () => {
this.frameId = requestAnimationFrame(this.tick)
}
tick = () => {
const timeLeft = getTimeRemaining(this.props.expiresAt)
if (timeLeft.total <= 0) {
this.stop()
// dispatch any other actions to do on expiration
} else {
// dispatch anything that might need to be done on every tick
this.setState(
{ timeLeft },
() => this.frameId = requestAnimationFrame(this.tick)
)
}
}
stop = () => {
cancelAnimationFrame(this.frameId)
}
render() {...}
}

setTimeout in React Native

I'm trying to load a splash screen for an iOS app built in React Native. I'm trying to accomplish this through class states and then a setTimeout function as follows:
class CowtanApp extends Component {
constructor(props){
super(props);
this.state = {
timePassed: false
};
}
render() {
setTimeout(function(){this.setState({timePassed: true})}, 1000);
if (!this.state.timePassed){
return <LoadingPage/>;
}else{
return (
<NavigatorIOS
style = {styles.container}
initialRoute = {{
component: LoginPage,
title: 'Sign In',
}}/>
);
}
}
}
The loading page works for a second, and then I guess when setTimeout tries to change the state to true, my program crashes: 'undefined is not an object (evaluating this.setState)'. I've been going at this for a couple of hours, any ideas on how to fix it?
Classic javascript mistake.
setTimeout(function(){this.setState({timePassed: true})}, 1000)
When setTimeout runs this.setState, this is no longer CowtanApp, but window. If you define the function with the => notation, es6 will auto-bind this.
setTimeout(() => {this.setState({timePassed: true})}, 1000)
Alternatively, you could use a let that = this; at the top of your render, then switch your references to use the local variable.
render() {
let that = this;
setTimeout(function(){that.setState({timePassed: true})}, 1000);
If not working, use bind.
setTimeout(
function() {
this.setState({timePassed: true});
}
.bind(this),
1000
);
Write a new function for settimeout. Pls try this.
class CowtanApp extends Component {
constructor(props){
super(props);
this.state = {
timePassed: false
};
}
componentDidMount() {
this.setTimeout( () => {
this.setTimePassed();
},1000);
}
setTimePassed() {
this.setState({timePassed: true});
}
render() {
if (!this.state.timePassed){
return <LoadingPage/>;
}else{
return (
<NavigatorIOS
style = {styles.container}
initialRoute = {{
component: LoginPage,
title: 'Sign In',
}}/>
);
}
}
}
const getData = () => {
// some functionality
}
const that = this;
setTimeout(() => {
// write your functions
that.getData()
},6000);
Simple, Settimout function get triggered after 6000 milliseonds
In case anyone wants it, you can also make the timer async and await it:
export const delay = (ms) => new Promise((res) => setTimeout(res, ms));
Usage:
// do something
await delay(500); // wait 0.5 seconds
// do something else
Change this code:
setTimeout(function(){this.setState({timePassed: true})}, 1000);
to the following:
setTimeout(()=>{this.setState({timePassed: true})}, 1000);
On ReactNative .53, the following works for me:
this.timeoutCheck = setTimeout(() => {
this.setTimePassed();
}, 400);
'setTimeout' is the ReactNative library function.
'this.timeoutCheck' is my variable to hold the time out object.
'this.setTimePassed' is my function to invoke at the time out.
You can bind this to your function by adding .bind(this) directly to the end of your function definition. You would rewrite your code block as:
setTimeout(function () {
this.setState({ timePassed: true });
}.bind(this), 1000);
Never call setState inside render method
You should never ever call setState inside the render method. Why? calling setState eventually fires the render method again. That means you are calling setState (mentioned in your render block) in a loop that would never end. The correct way to do that is by using componentDidMount hook in React, like so:
class CowtanApp extends Component {
state = {
timePassed: false
}
componentDidMount () {
setTimeout(() => this.setState({timePassed: true}), 1000)
}
render () {
return this.state.timePassed ? (
<NavigatorIOS
style = {styles.container}
initialRoute = {{
component: LoginPage,
title: 'Sign In',
}}/>
) : <LoadingPage/>
}
}
PS Use ternary operators for cleaner, shorter and readable code.
import React, {Component} from 'react';
import {StyleSheet, View, Text} from 'react-native';
class App extends Component {
componentDidMount() {
setTimeout(() => {
this.props.navigation.replace('LoginScreen');
}, 2000);
}
render() {
return (
<View style={styles.MainView}>
<Text>React Native</Text>
</View>
);
}
}
const styles = StyleSheet.create({
MainView: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
export default App;
There looks to be an issue when the time of the phone/emulator is different to the one of the server (where react-native packager is running). In my case there was a 1 minute difference between the time of the phone and the computer. After synchronizing them (didn't do anything fancy, the phone was set on manual time, and I just set it to use the network(sim) provided time), everything worked fine. This github issue helped me find the problem.
Same as above, might help some people.
setTimeout(() => {
if (pushToken!=null && deviceId!=null) {
console.log("pushToken & OS ");
this.setState({ pushToken: pushToken});
this.setState({ deviceId: deviceId });
console.log("pushToken & OS "+pushToken+"\n"+deviceId);
}
}, 1000);

Categories

Resources