React: state doesn't update in setInterval

Hi people,

I’m quite new to React, so my problem might be quite simple to fix. I’m building a snake game, where the snake and current direction are saved as state. A start button sets up the interval for updating the snake depending on the current direction, but … snake doesn’t want to update. I’m sure I’m missing something very basic. Here’s the relevant parts of code:

function Game(){

    const size = 15;

    const [snake, setSnake] = useState([95,94,93]);
    const [dir, setDir] = useState('right');
    const [gameInterval, setGameInterval] = useState([]);
    const [isGameRunning, setIsGameRunning] = useState(false);

    // add keyboard event listener on first render
    useEffect(()=>{
        document.addEventListener('keyup', handleKeyUp);
        return ()=>document.removeEventListener('keyup', handleKeyUp)
    },[]);

    function handleKeyUp(e){
        let newDir;
        if (e.keyCode === 37){
            newDir = 'left';
        } else if (e.keyCode === 38){
            newDir = 'up';
        } else if (e.keyCode === 39){
            newDir = 'right';
        } else if (e.keyCode === 40){
            newDir = 'down';
        } else {
            return;
        };
        setDir(newDir);
    };


    // toggle game on/off
    function handleStartBtn(){
        if (!isGameRunning){
            setIsGameRunning(true);
            if (gameInterval.length === 0){

                // moveSnake gets called every 800ms,
                // but doesn't update the snake

                let interval = setInterval(moveSnake, 800);
                setGameInterval([...gameInterval, interval])
            }
        } else if (isGameRunning){
            setIsGameRunning(false);
            if (gameInterval.length !== 0){
                gameInterval.forEach(int => clearInterval(int));
                setGameInterval([]);
            }
        }
    }


    function moveSnake(){
        let newSnake = [...snake];

        console.log(newSnake)
        // this always logs the original position of the snake on every run

        let head = newSnake[0];
        newSnake.pop();
        if (dir === 'right'){
            head += 1;
        } else if (dir === 'left'){
            head -= 1;
        } else if (dir === 'up'){
            head -= size;
        } else if (dir === 'down'){
            head += size;
        };
        newSnake.unshift(head);

        console.log(newSnake)
        // this logs the correct new position of the snake

        setSnake(newSnake);
    }




    // render stuff

    const boardVals = {size, apple, snake};
    const btnDisplay = isGameRunning ? 'Stop' : 'Start';

    return (
        <div id="game">
            <Board vals={boardVals}/>
            <div>
       
                // I'm rendering state to check what's happening:
                // Here, snake gets updated correctly. But only once.

                {snake.map((segment,i) => <span key={i}>{segment} </span>)}
                <p>{dir}</p>

                <button className="btn" onClick={handleStartBtn}>{btnDisplay}</button>
            </div>
        </div>
    )
}

export default Game;

Any hints are most welcome, I don’t even know what to google anymore…

(First time posting btw, so hello :wave:)

Hi @jsdisco. Welcome to FCC. I am not sure i understand what you are trying to achieve. It is best you create a codesandbox so that whoever wishes to help can easily play with your code.

Hello @jsdisco, there’s a wonderful explanation of your issue by Dan Abramov, you can find it here on his blog: overreacted.io .

I think that the reason why your function never updates is that your closure is always referencing the first render, so snake position is always the same.

As @nibble mentioned, a demo would greatly help in this regard.
Hope it helps.

The setInterval is a side effect. If your component rerenders, which it will do every time the state changes, you’re going back to the start again. The setInterval needs to be inside a useEffect to actually do anything, and the setInterval callback should be updating some state. Do read the article that @Marmiz posted, it goes into depth on this. React isn’t really designed to handle intervals easily, it needs some careful thought when you’re structuring the code

Awesome, that article addresses my problem exactly. I’ll need some time to get my head around it but I’m sure I can solve the issue now.