Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 851
Adam Avatar asked Sep 27 '19 11:09

Adam


People also ask

Can I use setInterval inside useEffect?

Using setInterval inside React components allows us to execute a function or some code at specific intervals. Let's explore how to use setInterval in React. The TL;DR: useEffect(() => { const interval = setInterval(() => { console.

Does useLayoutEffect run before useEffect?

But here if you check the console, "I will run first", that is, the useLayoutEffect actually runs before the useEffect runs. This is because useLayoutEffect is fired synchronously after DOM is mutated and before the browser paints the new changes.

How do you stop setInterval in useEffect?

To solve this, we can use useEffect's cleanup function, which looks like this: const Blinker = ({ text }) => { const [visible, setVisible] = useState(true); useEffect(() => { const intervalId = setInterval(() => { console. log(`Current blinking text: ${text}`); setVisible((visible) => !

Should I use useState or useEffect?

The useState hook is used for storing variables that are part of your application's state and will change as the user interacts with your website. The useEffect hook allows components to react to lifecycle events such as mounting to the DOM, re-rendering, and unmounting.


3 Answers

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);
like image 188
James Avatar answered Oct 20 '22 14:10

James


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.

like image 39
Dehan Avatar answered Oct 20 '22 15:10

Dehan


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

like image 3
Pavlo Avatar answered Oct 20 '22 16:10

Pavlo