Adam
Adam

Reputation: 20882

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.

enter image description here

Upvotes: 11

Views: 11668

Answers (4)

Reinhard
Reinhard

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

0DDC0
0DDC0

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

Pavlo
Pavlo

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

James
James

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

Related Questions