React native setInterval useState - reactjs

I'm creating interval counter and below code is working fine. But I have few questions about this code which I do not understand.
import React, { useState, useEffect } from 'react';
import {View, Text} from 'react-native'
const Interval = () => {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
console.log(`seconds: ${seconds}`)
setSeconds(seconds => seconds + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<View>
<Text>
{seconds} seconds have elapsed since mounting.
</Text>
</View>
);
};
export default IntervalExample;
Why this is not working if I put setSeconds(seconds => seconds + 1); instead setSeconds(seconds + 1); more simply ?
Why console.log(`seconds: ${seconds}`) is always log as 0 ?

to run useEffect You need to pass variables as the second parameter (in your case, it is seconds). When this variable has changed then useEffect will be run again.
From docs:
If you pass an empty array ([]), the props and state inside the effect will always have their initial values. While passing [] as the second argument is closer to the familiar componentDidMount and componentWillUnmount mental model, there are usually better solutions to avoid re-running effects too often. Also, don’t forget that React defers running useEffect until after the browser has painted, so doing extra work is less of a problem.
that is way, You need to pass second varable to useEfect:
useEffect(() => {...}, [seconds])
and in that case You can use setSeconds(seconds + 1); instead of passing function.
Complete code:
useEffect(() => {
const interval = setInterval(() => {
console.log(`seconds: ${seconds}`)
setSeconds(seconds + 1)
}, 1000)
return () => clearInterval(interval)
}, [seconds])

use this ;
useEffect(() => {
const interval = setInterval(() => {
setSeconds(seconds + 1);
console.log(seconds)
}, 1000);
return () => clearInterval(interval);
}, [seconds]);

Related

Adding seconds to Intl.dateTimeFormat with React useEffect, useState, and setInterval

I have come across a weird problem where changing the order of a clone inside a setState() hook function changes the expected behavior.
I am trying to add one second to the value each second. However doing this directly causes the seconds to increase by two instead of one.
This works
const [value, setValue] = useState(new Date());
useEffect(() => {
const interval = setInterval(
() =>
setValue((value) => {
const clonedDate = new Date(value.getTime());
clonedDate.setSeconds(clonedDate.getSeconds() + 1); // Add one second to the time
return clonedDate;
}),
1000
);
return () => {
clearInterval(interval);
};
}, []);
This adds two seconds instead of one
const [value, setValue] = useState(new Date());
useEffect(() => {
const interval = setInterval(
() =>
setValue((value) => {
value.setSeconds(value.getSeconds() + 1);
const clonedDate = new Date(value.getTime());
return clonedDate;
}),
1000
);
return () => {
clearInterval(interval);
};
}, []);
The only thing that is clear to me as that in the second version a state mutation is obviously occurring, but to be honest it isn't clear to me exactly where. It seems as though even though you are creating a new javascript Date object that it is still referencing properties of the previous data object.
Consider the following examples that exhibit identical behavior:
function App() {
const [value1, setValue1] = useState({ c: 0 });
const [value2, setValue2] = useState({ c: 0 });
useEffect(() => {
const interval = setInterval(
() =>
setValue1((value) => {
const clonedValue = { ...value }; // shallow copy first
clonedValue.c = clonedValue.c + 1; // update copy
return clonedValue; // return copy
}),
1000
);
return () => {
clearInterval(interval);
};
}, []);
useEffect(() => {
const interval = setInterval(
() =>
setValue2((value) => {
value.c = value.c + 1; // mutate current
const clonedValue = { ...value }; // shallow copy
return clonedValue; // return copy
}),
1000
);
return () => {
clearInterval(interval);
};
}, []);
return (
<div className="App">
<h1>Non-mutaiton Version</h1>
{value1.c}
<h1>Mutation Version</h1>
{value2.c}
</div>
);
}
Interestingly though, if you remove the React.StrictMode from around App the two perform identically.
StrictMode currently helps with:
Identifying components with unsafe lifecycles
Warning about legacy string ref API usage
Warning about deprecated findDOMNode usage
Detecting unexpected side effects
Detecting legacy context API
Detecting unexpected side effects
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer
Demo with your original date objects running in both StrictMode and non-StrictMode:
The issue is with below line it is mutating the value object. You don't need to call value.setSeconds, value.getSeconds()+1 itSelf is enough to increment seconds by 1.
Replace below line of code -
const interval = setInterval(
() =>
setValue2((value2) => {
let second = value2.getSeconds() +1;
//value2.setSeconds(second);
let newTime = new Date();
newTime.setSeconds(second);
const clonedDate = new Date(newTime.getTime());
return clonedDate;
}),
1000
);

React. Fill a state array every N seconds, and only while its smaller than X length

I have the following code
import * as React from "react";
import { useState, useEffect } from "react";
const TxContainer: React.FunctionComponent = (props) => {
const [tx, setTx] = useState<Array<string>>([]);
useEffect(() => {
const interval = setInterval(() => {
setTx((oldArr) => [...oldArr, "tx" + Math.random()]);
}, Math.floor(Math.random() * 3000) + 1000);
return () => clearInterval(interval);
}, [tx.length < 10]); //this useEffect still keeps pushing even if the array is bigger than 9
useEffect(() => {
console.log(tx.length);
}, [tx.length]);
let listTx = tx.map((data, index) => (
<p key={index}>
{index} {data}
</p>
));
return <React.Fragment>{listTx}</React.Fragment>;
};
export default TxContainer;
Im trying to create random strings and put them in an array until this array is 10. Whenever, its 10 it should start deleting the first element of the array to keep working and displaying new data, but thats another story.
The point is that its not stopping when it should.
When you are passing a compare like that in the dependency array of the useEffect it will only invoke it again, when the resulting value changes. Basically those dependencies mean "if this value changes in any way, call the useEffect". Besides that it's being called on mount and on unmount. So for your code to work as you intended you should pass to the dependency array a variable that your calculations depend on and pass the logic inside the useEffet.
Here's an example of the code that you described, of course I don't know if you want to use the same timeout or not.
useEffect(() => {
let interval;
if (tx.length < 5) {
interval = setInterval(() => {
setTx(oldArr => [...oldArr, "tx" + Math.random()]);
}, Math.floor(Math.random() * 3000) + 1000);
} else {
interval = setInterval(() => {
setTx(oldArr => [...oldArr.slice(1)]);
}, Math.floor(Math.random() * 3000) + 1000);
}
return () => clearInterval(interval);
}, [tx.length ]);`
Here is the solution: second useEffect is not needed. This effect runs first time and whenever array length is changed. when it reaches 11, clear the interval or remove first element.
useEffect(() => {
const interval = setInterval(() => {
setTx((oldArr) => [...oldArr, "tx" + Math.random()]);
}, Math.floor(Math.random() * 3000) + 1000);
if(tx.length === 11){
setTx(prev=>prev.slice(1))
// clearInterval(interval)
}
return () => clearInterval(interval);
}, [tx.length]);

Custom Hooks SetInterval

I'm following this article by Dan Abramov:
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
In the article, Dan makes a custom useInterval hook, to create a dynamic setInterval.
The hook looks like this:
export default function useInterval(callback, delay) {
//this useInterval function will be called whenever the parent component renders.
// on render, savedCallback.current gets set to whatever the callback is, if the callback
// has changed
const savedCallback = useRef();
console.log("called")
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
/**
* Likewise, the set interval is set off,
* and if delay is
*/
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => {
console.log("clearEed!")
clearInterval(id);
}
}
}, [delay]);
}
There's a part I don't understand though, which is here:
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => {
console.log("clearEed!")
clearInterval(id);
}
}
}, [delay]);
I understand that this useEffect is called if the delay is changed. The callback is assigned to tick, then if the delay isn't null, id is set to the SetInterval, with tick and the delay as parameters. This all makes sense. But what happens next is strange to me. I know useEffect can take a return statement for when the component unmounts, but why are we clearing the interval we set just before? I'd really appreciate it if someone could talk me through this.
In particular, I'd really like help understanding these lines:
if (delay !== null) {
let id = setInterval(tick, delay);
return () => {
console.log("clearEed!")
clearInterval(id);
}
}
I'm using it like this:
function TimerWithHooks() {
let [count, setCount] = useState(0);
let [delay, setDelay] = useState(1000);
useInterval(() => {
setCount(count + 1);
}, delay)
const handleDelayChange = evt => {
setDelay(Number(evt.target.value))
}
return (
<>
<h1>{count}</h1>
<input value={delay} onChange={handleDelayChange} />
</>
);
}
export default TimerWithHooks;
Effects with Cleanup
When exactly does React clean up an effect?
React performs the cleanup when the component unmounts. However, as we
learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time. We’ll discuss why this helps avoid bugs
and how to opt out of this behavior in case it creates performance
issues later below.
This means that every time the delay changes, the Effect will cleanup previously effects, thus it will clear the timer every time we change the delay and NOT when the component unmounts. This way, we can adjust the timer dynamically without having to worry about clearing the timers.
I guess Dan clear timer when the component will unmount, but I think beater make this after the function executed. something lick this:
useEffect(() => {
if (delay !== null) {
let timerId = setInterval(
() => {
savedCallback.current();
clearInterval(timerId);
},
delay
);
}
}, [delay]);

React UseEffect Example Not Working Reason

Why does this not work as a normal one second counter?
function UseEffectBugCounter() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <div>The count is: {count}</div>;
}
Example: https://codesandbox.io/s/sparkling-rgb-6ebcp
-Is it because of stale closures?
or
-Is it because the count is a state variable and the component would be re-rendered after the state update so a new interval will be created creating some sort of loop?
or
-Is it something else?
I'm looking for a why this occurs in this answer if possible, there are a few different articles stating why it doesn't work (as per above). But none have been able to provide a good argument so far.
You can use callback for set state to use latest counter value:
setCount(count => (count + 1));
You may need to add the dependency for count in useEffect. Currently useEffect is only called on the mount and is not called after that (i.e when the count value changes).
So it always says 0 because useEffect is executed only once ( on mount ) and that time the count value is set to 0. And thus it every time it logs 0 on setInterval due to closure.
I have updated the code sandbox to find the reason and meaningful logs. Here is the sandbox link: https://codesandbox.io/s/admiring-thompson-uz2xe
How to find closure value: You can check the logs and traverse through prototype object to find [[Scopes]] and you will get the values as seen in the below screenshot:
This would work:
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
}, [count]);
You can check this doc: https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
You can read this as well: You can read this as well: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Hope this helps!
If you removed second params for useEffect your application will be rendered always if u have some change in state, but this is bad practice. You need in second params choose for wich parametrs you need watch ...
Example with choosen params:
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
}[count]);
Without
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
});
Because you didn't add count to useEffect's dependences, then inside the effect count always is 0.
You should use useReducer to resolve your problem:
function UseEffectBugCounter() {
const [count, dispatchCount] = React.useReducer((state, { type }) => {
switch(type) {
case 'inc':
return state + 1
default:
return state
}
}, 0);
React.useEffect(() => {
const intervalId = setInterval(() => {
dispatchCount({ type: 'inc'})
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <div>The count is: {count}</div>;
}
You need to pass count instead of blank array in useEffect
function UseEffectBugCounter() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
console.log(count);
}, 1000);
return () => clearInterval(intervalId);
},[count]);
return <div>The count is: {count}</div>;
}

React useEffect with useState and setInterval

using the following code to rotate an array of object through a component DOM. The issue is the state never updates and I can't workout why..?
import React, { useState, useEffect } from 'react'
const PremiumUpgrade = (props) => {
const [benefitsActive, setBenefitsActive] = useState(0)
// Benefits Details
const benefits = [
{
title: 'Did they read your message?',
content: 'Get more Control. Find out which users have read your messages!',
color: '#ECBC0D'
},
{
title: 'See who’s checking you out',
content: 'Find your admirers. See who is viewing your profile and when they are viewing you',
color: '#47AF4A'
}
]
// Rotate Benefit Details
useEffect(() => {
setInterval(() => {
console.log(benefits.length)
console.log(benefitsActive)
if (benefitsActive >= benefits.length) {
console.log('................................. reset')
setBenefitsActive(0)
} else {
console.log('................................. increment')
setBenefitsActive(benefitsActive + 1)
}
}, 3000)
}, [])
the output I get looks like the following image. I can see the useState 'setBenefitsActive' is being called but 'benefitsActive' is never updated.
You pass no dependencies to useEffect meaning it will only ever run once, as a result the parameter for setInterval will only ever receive the initial value of benefitsActive (which in this case is 0).
You can modify the existing state by using a function rather than just setting the value i.e.
setBenefitsActive(v => v + 1);
Some code for your benefit!
In your useEffect as #James suggested, add a dependency to the variable that's being updated. Also don't forget to clean up your interval to avoid memory leaks!
// Rotate Benefit Details
useEffect(() => {
let rotationInterval = setInterval(() => {
console.log(benefits.length)
console.log(benefitsActive)
if (benefitsActive >= benefits.length) {
console.log('................................. reset')
setBenefitsActive(0)
} else {
console.log('................................. increment')
setBenefitsActive(benefitsActive + 1)
}
}, 3000)
//Clean up can be done like this
return () => {
clearInterval(rotationInterval);
}
}, [benefitsActive]) // Add dependencies here
Working Sandbox : https://codesandbox.io/s/react-hooks-interval-demo-p1f2n
EDIT
As pointed out by James this can be better achieved by setTimeout with a much cleaner implementation.
// Rotate Benefit Details
useEffect(() => {
let rotationInterval = setTimeout(() => {
console.log(benefits.length)
console.log(benefitsActive)
if (benefitsActive >= benefits.length) {
console.log('................................. reset')
setBenefitsActive(0)
} else {
console.log('................................. increment')
setBenefitsActive(benefitsActive + 1)
}
}, 3000)
}, [benefitsActive]) // Add dependencies here
Here, a sort of interval is created automatically due to the useEffect being called after each setTimeout, creating a closed loop.
If you still want to use interval though the cleanup is mandatory to avoid memory leaks.
When you pass a function to setInterval, you create a closure, which remembers initial value of benefitsActive. One way to get around this is to use a ref:
const benefitsActive = useRef(0);
// Rotate Benefit Details
useEffect(() => {
const id = setInterval(() => {
console.log(benefits.length);
console.log(benefitsActive.current);
if (benefitsActive.current >= benefits.length) {
console.log("................................. reset");
benefitsActive.current = 0;
} else {
console.log("................................. increment");
benefitsActive.current += 1;
}
}, 3000);
return () => clearInterval(id);
}, []);
Demo: https://codesandbox.io/s/delicate-surf-qghl6
I've had the same problem and found a perfect solution on
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
using an own hook
import { useRef, useEffect } from "react";
export function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
using it like
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, 1000);

Resources