Reputation: 20882
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.
Upvotes: 11
Views: 11668
Reputation: 1746
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);
Upvotes: 4
Reputation: 5179
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.
Upvotes: 12
Reputation: 44889
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
Upvotes: 4
Reputation: 82096
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);
Upvotes: 17