React - handling state with useEffect

Hi, it’s that Snake again giving me headaches.

Here’s a codesandbox with only the relevant parts:

The SnakeGame component holds the state that is relevant for the game (snake position, direction, intervalTime).

I’m using useEffect with an empty array to attach the keyboard event listeners which set the direction of the snake.

The App component holds the two states of the game (isStart, isPause) and passes them down as props to the SnakeGame component.

Depending on a combination of those two Booleans, SnakeGame can either be in “running” mode, in “pause” mode or in “stop/reset” mode.

The bug of the game is now that the user can change the direction of the snake in all modes. Naturally, I only want the direction to be set when the game is in “running” mode. And I can’t for the hell of it figure out how to do that, because I can’t add any dependencies to the useEffect hook which attaches the event listeners. I don’t want to remove the listeners on every pause/stop of the game, I only want to be able to define a condition whether the direction should be set or not.

I’ve been struggling with this for days. What I really like about React that it seems to force “separation of concerns” on me, which is nice because my functions have a tendency to be a bit messy, but it’s really causing me trouble in this case.

Just after a quick glance…

Can you have handleKeyUp decide whether or not to execute the switch statement whether or not the game is in running mode? Before the switch, something like:

if (!isRunningMode) {
  return;
}

That would be my suggestion as well to check the state inside the handler and just return out if the game is not running. However, I think the state management might need a little love and some cleanup.

Well that’s the problem, if I add any variables to the useEffect that’s attaching the event handlers, I obviously can’t useuseEffect with an empty array. Because it would depend on other state variables like isStart or isPause. But I have to use an empty array because otherwise, I’d have to attach and deattach and reattach the event handlers each time state changes.

I’ve added your suggestion to the codesandbox, watch React complaining about missing dependencies :expressionless:

Wut? The state management is excellent and super clean! :ok_hand:

I’m not understanding. I’m not talking about adding anything to useEffect, I’m talking about adding logic to handleKeyUp. The useEffect callback would work exactly the same.

I have no idea either what’s the misunderstanding, but I feel something’s about to click big time over here. I’ll copy the relevant parts of the code again for easier reference:

Here’s my useEffect, all it depends on is the handleKeyUp callback:

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

Now the callback. As you suggested, I added an if statement at the top to make sure that it only sets the direction of the snake if the game is currently running:

  function handleKeyUp(e) {
    if (currGameMode === "running") {
      let newDir;
      switch (e.keyCode) {
        case 37: newDir = "left"; break;
        case 38: newDir = "up"; break;
        case 39: newDir = "right"; break;
        case 40: newDir = "down"; break;
        default: return;
      }
      setDir(newDir);
    } else {
      return;
    }
  }

With this modification, React now marks my empty array in my useEffect and complains that it’s missing a dependency (handleKeyUp).

Moving the handleKeyUp function into the useEffect callback of course doesn’t help, it only shifts the problem, because currGameMode depends on the component’s state variables (isStart/isPause), so React now wants those as dependencies, too.

Can someone please stop me from running in circles…

Coming back to this for a moment, it’s true that some parts of state seem to be all over the place; App renders a Header component with the start/stop buttons, in my local version also a Controls component to toggle the theme and difficulty, all of which are actually things that only the Game component needs.

I’ve done that fully intentionally, because making stuff work in React isn’t my problem - I can do all the things in React that I can do in JavaScript. If it all sits in one file. My problem with React is getting a deeper understanding about how to share state between components. It may seem a bit forced to split it all up like that, but well it’s just for the purpose of learning.

OK, OK, I see.

I’m still relatively new to hooks myself and am still learning. Yeah, they whole thing of demanding certain things be included in the dependency array has seemed odd to me, especially functions.

Here is an explanation of it that made a light go on for me. At the end he mentions the hook useCallback that may be something you could use here.

1 Like

I’ve gone through that article very carefully, but I don’t think it’s the solution. I was able to refactor my code so that it SEEMINGLY does what it should (only change direction of the snake if currGameMode === ‘running’):

KeyHandler with useCallback hook:

const keyHandler = useCallback(e => {

        function handleKeyUp(e){
            if (currGameMode !== 'running'){
                return;
            }

            let newDir;
            switch(e.keyCode){
                case 37: newDir = 'left'; break;
                case 38: newDir = 'up'; break;
                case 39: newDir = 'right'; break;
                case 40: newDir = 'down'; break;
                default: return;
            }
            setDir(newDir);
        };
        handleKeyUp(e);
    }, [currGameMode]);

Attaching the event listener:

useEffect(() => {
        document.addEventListener('keyup', keyHandler);
        return () => document.removeEventListener('keyup', keyHandler);
    }, [keyHandler]);

Again, I’ve only shifted my problem. I can’t useEffect with an empty array, because the handler closes over my currGameMode variable no matter what. So what I’m doing at this point is adding and removing the event listener each time the value of currGameMode is changing. Which is sort of what I wanted to avoid in the first place. I also don’t think that this is how it should be or would be done in a real application.

Given the fact that this seems to be a rather simple scenario, I find it unexpectedly hard to solve :neutral_face:

Hi @jsdisco. I haven’t read the entire thread so I am not 100% sure what you are trying to achieve here. By looking at the quoted statement above , you are trying to counter a feature built intentionally in react to prevent bugs. In the above scenario what is the reason for trying to avoid passing keyHandler as a dependency? It is a requirement for you to pass an event handler used in useEffect as a dependency. You can look at this stackeoverflow question. There are interesting answers which might be relevant to your case.

Each time keyHandler changes (which will happen each time my currGameMode variable changes), I’m detaching and reattaching the keyboard event listener, which seems like an awful practice to me.

I’ve found a solution for now, though. Instead of keeping my currGameMode in a variable, I useRef to put it into a mutable object and access the current status through currGameMode.current. This way, currGameMode doesn’t change, only its value does, so
a) currGameMode doesn’t need to be included into the dependency array of the useEffect that adds the event listener and
b) the handleKeyUp function can access the currGameMode.current value and decide whether the direction should be set or not.

const currGameMode = useRef();
useEffect(()=>{
        if (isStart && !isPause){
            currGameMode.current = 'running';
        } else if (isStart && isPause){
            currGameMode.current = 'paused';
        } else {
            currGameMode.current = 'stopped';
        }
})
function handleKeyUp(e){
    if (currGameMode.current !== 'running'){
        return;
    }
    let newDir;
    switch(e.keyCode){
        case 37: newDir = 'left'; break;
        case 38: newDir = 'up'; break;
        case 39: newDir = 'right'; break;
        case 40: newDir = 'down'; break;
        default: return;
    }
    setDir(newDir);
};
useEffect(() => {
    document.addEventListener('keyup', handleKeyUp);
    return () => document.removeEventListener('keyup', handleKeyUp);
}, []);

However, there still seems to be something wrong about this approach. It’s mentioned in the docs (and also the link you’ve posted) that useRef is somewhat the “last resort” in this case if nothing else works. Given that this seems to be a very common use case, I guess there’s a better solution, but for now it works for me.

Hi @jsdisco,

Depending on a combination of those two Booleans, SnakeGame can either be in “running” mode, in “pause” mode or in “stop/reset” mode.
The bug of the game is now that the user can change the direction of the snake in all modes. Naturally, I only want the direction to be set when the game is in “running” mode.

I think that you are trying to write a finite state machine:

  • “When Booleans Are Not Enough… State Machines?”

In this article there are examples of finite state machines writen in vanilla JavaScript and Reactjs Robust react user interfaces with finite state machines .

From the article:

/*
 * coded by: David Khourshid 
 * https://codepen.io/davidkpiano/pen/xPgBqr
 *
 */

const machine = {
  green: {
    TIMER: 'yellow'
  },
  yellow: {
    TIMER: 'red'
  },
  red: {
    TIMER: 'green'
  }
};

let currentState = 'green';

// The finite state machine transition function
function transition(state, action) {
  return machine[state][action];
}

// Any side effects
function update(state) {
  currentState = state;
  document.getElementById('traffic-light')
    .setAttribute('data-state', state);
}

setInterval(() => {
  const nextState = transition(currentState, 'TIMER');
  update(nextState);
}, 2000);

document.getElementById('timer')
  .addEventListener('click', () => {
    const nextState = transition(currentState, 'TIMER');
    update(nextState);
  });

You can see that the idea of your cade is similar to the above example (use the current state to decide what to do).

However, there still seems to be something wrong about this approach. It’s mentioned in the docs (and also the link you’ve posted) that useRef is somewhat the “last resort” in this case if nothing else works.

Maybe you can use context plus a custom Hook and that way avoid the use of useRef ( I use a library, so I never have tried to write a finite state machine in Reactjs).

The library that I use is: Xstate, this is an example of a finite state machine[0]:

The demo:

https://diegoperezm.github.io/simple-machine-config-xstate/examples/browser/freecodecamp/calculatorv2/index.html

Cheers and happy coding :slight_smile:

Notes:
[0] I wrote the script that generate the graph ( represents the fsm ) and the other script that let me use a state transition table, but you can use Xstate alone without a problem.

Not sure if I am, my case is even simpler than the traffic light in the example, I have two Booleans and derive three states from them, and I can’t see at the moment how a FSM would fix my problem (interesting concept, nevertheless). But wouldn’t it also just shift it, because my Keyboard Handler would still depend on state variables, whether those are Booleans or something that my FSM returns?

I wanted to avoid useContext for now, and solve the problem with only state and props (not because it’s preferrable, only because I like to learn one thing at a time). I also don’t really understand why useRef is called “the last resort”, I lack the experience to tell why it shouldn’t be appropriate to use it or why it should be avoided.

As for custom hooks, I’m slowly growing into understanding them. Obviously not enough yet to even tell whether that could be a solution.

Cheers back :vulcan_salute:

1 Like