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.

1 Like

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

2 Likes

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.