Home > Software engineering >  How to set state with a setInterval() when a button is clicked?
How to set state with a setInterval() when a button is clicked?

Time:10-16

I'm trying to update the count of activeIndex within the setInterval when the checkbox is ticked. handleNext() and handlePrevious() are working fine when buttons are clicked but when the checkbox is checked the value of activeIndex is not getting updated in handleNext() but it's getting updated on the screen, so there is no condition check for activeIndex and it goes beyond 3.

import { useState } from "react";
import "./styles.css";
    
export default function App() {
    const [slideTimer, setSlideTimer] = useState(null);
    const [activeIndex, setActiveIndex] = useState(0);
    const slideDuration = 1000;
    
    const handleNext = () => {
        if ((activeIndex) >= 3) {
            setActiveIndex(0);
        } else {
            setActiveIndex(prev => prev   1);
        }
    };
    
    const handlePrev = () => {
        if (activeIndex <= 0) {
            setActiveIndex(3);
        } else {
            setActiveIndex((prev) => prev - 1);
        }
    };
    
    const toggleSlider = () => {
        if (slideTimer) {
            clearInterval(slideTimer);
            setSlideTimer(null);
            return;
        }

        setSlideTimer(setInterval(handleNext, slideDuration));
    };
    
    return (
        <div className="App">
            <h1>{activeIndex}</h1>
            <button onClick={() => handleNext()}>Next</button>
            <button onClick={() => handlePrev()}>Previous</button>
            <input onClick={() => toggleSlider()} type="checkbox" />
        </div>
    );
}

I have tried putting the code for toggleSlider inside useEffect() but that too is not working.

CodePudding user response:

Your problem is that when handleNext gets defined (which occurs on every rerender), it only knows about the variables/state in its surrounding scope at the time that it's defined. As a result, when you queue an interval with:

setInterval(handleNext, slideDuration)

You will be executing the handleNext function that only knows about the component's state at that time. When your component eventually rerenders and sets a new value for activeIndex, your interval will still be exeucting the "old" handleNext function defined in the previous render that doesn't know about the newly updated state. One option to resolve this issue is to make the hanldeNext function not rely on state obtained from its outer scope, but instead, use the state setter function to get the current value:

const handleNext = () => {
  setActiveIndex(currIndex => (currIndex    1) % 4);
};

Above I've used % to cycle the index back to 0, but you very well can also use your if-statement, where you return 0 if currIndex >= 3, or return currIndex 1 in your else.

I would also recommend that you remove the slideTimer. This state value isn't being used to describe your UI (you can see that as you're not using slideTimer within your returned JSX). In this case, you're better off using a ref to store your interval id:

const slideTimerRef = useRef(0); // instead of the `slideTimer` state
...
clearInterval(slideTimerRef.current);
slideTimerRef.current = null;
...
slideTimerRef.current = setInterval(handleNext, slideDuration);
  • Related