Advanced React Hooks Concepts — through Snake!

Mark Romano
13 min readJun 11, 2019
Motorola RAZR flashbacks

Last week I wrote a piece on some interesting things I learned from a blog post by Dan Abramov. One of those things was the special ability dispatch has in a useEffect callback, whereby actions dispatched in the callback invoke your reducer, which can make use of a constantly changing state variable without calling useEffect again, ensuring the cleanup function is only ran once. You can see a simple example here. If you want more background, I’d recommend you go back and read my quick piece, or if you have some serious time to kill, Dan’s post.

Even after getting a grasp on this function that dispatch offered, I struggled to think of a common use case case for this. A few days later, I was on a 3 hour flight, with no book, no internet, and no cell phone games. To kill time, I busted out my laptop, opened up an existing react app, and created a new route — /snek. I decided I wanted to build a snake game with hooks. I’ve created snake in the past with vanilla JS, so I figured this should be a snap. Pretty quickly I learned it would not be, as I ran into some bumps. But funnily enough, I used my takeaways from Dan’s piece to overcome my issues. Pretty, pretty, pretty, cool. While I imagine snake isn’t a super popular use case for the application of dispatch , using setInterval certainly is, so I hope you can apply your learnings from here unto your own apps.

Snake in the browser is a very fun way to use our frontend skillz. It’ll push your Browser API, CSS, and JavaScript abilities. Mix in react hooks to the equation, and it becomes a really interesting experiment. In this exercise, we’ll will build part of a snake game. We will build a grid and a snake that moves every second in the direction applied via keypresses. No growing, and no apples. Snakes don’t eat apples anyway. This part of the game is all we’ll need to do to gain a better understanding of dispatch. First we’ll build it naively with just useState, like I did irl, and see what problems arise. Then, we’ll refactor with useReducer and see how this resolves our issues. Lastly, we’ll gather around our proverbial camp fire and sum everything up.

CSS and HTML Time — yay! 😑

Before we start using our hooks API, let’s talk about the snek grid. We’ll lay out an n by n grid, which will help us to position the head of our snek. We’ll represent this using an array of arrays. The subarrays will contain 2 numbers. The first will represent the column, the second the row. So our final data structure should look like this:

[ [ 0, 0 ],
[ 0, 1 ],
[ 0, 2 ],
[ 0, 3 ],
[ 0, 4 ],
[ 0, 5 ],
[ 0, 6 ],
[ 0, 7 ],
...//yadda yadda yadda
]

So something like [2,4] would represent the 3rd row, 5th column. We could hardcode this pretty easily with some copy pasta, but let’s be kewl and use es6 magic to build this array

const grid = [];[...Array(10).keys()].forEach(i => [...Array(10).keys()].reduce((outer, j) => grid.push([i, j])))

We’re using a handy trick to create an array of numbers from 0 to 9 to represent our rows, then iterate through those numbers to create 10 pairs of numbers per row, pushing the pairs into our grid array. If you know a slicker way to do this, please boast it in the comments 😎.

Now that we’ve declared our grid, lets lay down some JSX to make the grid and snek. The next couple steps are largely just styling, so don’t get super caught up in the details and feel free to help yourself to some copypasta. One important hooks-specific piece to check out is on line 8, where we invoke useState . We’re creating the piece of state that represents the current position of the snakes head, and giving it a default value of [0, 0] , meaning row 0, column 0. Graphically, this means our snake will start in the top left corner of our grid.

We’re going to nest a div inside of another. The outer div is a flexbox to hold and center it’s child. It’s child will act as a container of the grid. Each slot in the grid will be 34px wide and high. The div itself with be 30px, with 1px of border and 1px of margin on each side, making the total 34px. Thus, we set the max width and height of the grid to 34 times the number of rows we’ve created ( (30+2+2) * rowsLength ). Instead of adding the style object inline, I write smol helper functions below the return statement that will work because hoisting.

We’re going to create a Row and Tile component too. They are both rather simple presentational components, and I think it makes more sense to start with the more primitive — Tile.

Like I said, tile is a simple fella. But there is slice of logic in there. We’re going to pass a boolean prop called isActive in. This indicates if the head of the snake is currently atop this tile. If it is, we set the color of the tile to yellow, which will represent our snake.

But how do we determine the value of our isActive bool? We’ll do so with our Row component and the values the crazy grid array holds. The props we pass into Row are a little too abstract without some context, so let’s jump back into App and see how we invoke our rows

Once again we use our cool insta-array trick to create an array of numbers from 0 to 9. We then map over these numbers, creating a Row for each of them. We actually use the value to slice out the items in our grid that correspond to each row. We also want to pass the position state value in, so we can calculate the isActive bool we pass into Tile .

Like I mentioned, Row is pretty simple too. It’s task is to render an array of tiles and determine if one of them is the where the snake’s head is.

We’ll iterate through the values contained in rowVals , and with every iteration, we’ll check the values of x and y , the values contained in the subarrays of grid , against the values of currentX and currentY , the values of position . If they match, we’ve found our snek!

If we’ve created our data structures properly, applied appropriate logic for positioning elements, perfectly mapped our components to their corresponding divs, and painstakingly styled our app — a square with squares inside of it should render.

Eat yer heart out DaVinci 🎨

Applying Hooks! 🎣

Now that the styling and components have been laid out, let’s start hookin’ . We already have one state variable, position, to represent the position of the snake on the grid, but we’ll need another, direction , to indicate in what direction the head is moving. We’ll also have to use an effect to create listeners for keypresses that will change the value of direction.

We’ll invoke useState and pass in an initial value of right for our direction variable. We pass in a callback to useEffect that puts a listener on the keydown property on our document body . It filters for arrow key presses, and uses setDirection to change the value accordingly. Also note, we’re only running this effect once — on line 24 we pass in an empty array as the 2nd argument to useEffect, informing it there are no deps it should change upon.

Now to make the snek move. the values in the array the variable position hold determine where on the grid the snake head will appear. The first index represents the row, and the second represents the column. In snake, the head continuously moves in a direction determined by the player. This means, at a set interval, the values in the position array will have to be updated.

Arrays to pass into `setPosition` to move in each direction

In hooks-speak, this translates to an effect that kicks off a setInterval that will call setPosition with new values that represent the new position of our snek. We will want to call this effect every time the direction value changes, so we can pass in an array with the proper incremented/decremented row/column values. Since we’ll be calling this effect many times, we’ll want to cleanup each time, to remove the setInterval from our window .

Let’s bang this out. We’ll add this second effect, and inside the callback, we’ll call our setInterval and assign it to a variable. We’ll return a cleanup function that calls clearInterval. Inside the callback, let’s write a switch statement to check the values of direction, and pass an array into setPosition that represents a step in a given direction. Let’s throw a console log in there too, to print the value of position , because the the code I just laid out won’t function as we are thinking, because I’m trying to drive home a point 😏.

Check out the app in the browser. Refresh the page. You’ll notice our snake only moves once then stops. If you hit one of the arrow keys, the same thing will happen — it’ll move once then stop. Dafuq? We’ve set an interval to run a function that calls setPostion once a second; it’s strange that it only seems to run once.

Open the dev tools. You’ll see our log printing the position , and you’ll see it keeps printing the same coordinates. The nature of hooks is that each render is a “snapshot” in time. And at the snapshot that our setInterval was invoked, our position was a specific set of numbers. That’s just a dramatic way of saying that our variables don’t change until we explicitly tell them to.

0 0 5evr 😭

At the time of calling our setInterval callback, we frame position at whatever set of numbers that position is set at (0, 0 at first). Luckily, we have a way to hack the matrix; a tool in our arsenal that transcends time and space to get the true current value of position — the ability to pass a callback into setPosition . Just like setState accepts either an object literal or a callback whose argument passed in was a copy of our current state, our setPosition function has the same option available to it. The argument passed into the callback isn’t concerned with the snapshot — it holds the true, current value of position , and we can use this to fix our issue.

Passing in callbacks allows us to reference our closed-over state. While variables in the snapshot, like position, are immutable, the value passed into the callback is certainly not. Here, we destructure the argument, and return a new array, incrementing/decrementing either x or y depending on the value direction holds. cmd+tab back to that browser and watch the magic✨.

he move

You’ll notice once you run into a wall, your snake just disappears. This is because it’s out of bounds — there are no tiles on the grid whose values match the values in position . In real snake, if you go out of bounds, the game resets, and your head moves back to the original position. We can provide that function pretty easily.

With the added ternary, we check to make sure the head isn’t outside the defined bounds of our grid. If it is, we set position back to [0,0] . If not, it’s business as usual.

All is seemingly well. But use the arrow keys to move in another direction and look closely. Sure, it moves in the applied direction, but there seems to be an odd, variable delay. Why would this be? Look at the second argument we pass into the useEffect , the array that holds our deps. The array holds direction , so that every time the value of direction changes, the callback passed into useEffect is called again. This means that whenever we change direction, our interval is cleared and a new one is set. This is why the movement is not smooth — our timer is reset mid-interval; this creates the effect of a longer than usual delay.

So wut we do?! We can’t remove the direction dep; if we do, because of the snapshot nature of hooks, direction will be stuck at it’s initial value forever. This is where we’ll apply some not so obvious solutions hooks offers to situations like this.

Enter useReducer 💅

The useReducer function is a new feature of the hooks API that allows us to use a reducer approach to managing our state. From the official docs:

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.

Can’t disagree with the above. But one ability of the function they left out is that the dispatch method returned from useReducer acts as a backdoor out of snapshot land. This will allow you to remove your problematic dependency in useEffect, and instead, invoke dispatch inside the callback. dispatch is just a function that invokes your reducer and passes to it an object that acts as a set of instructions for changing the state. Your reducer can reference variables from useState inside of it, and those values will always be up to date.

What I described may sound kinda abstract and annoyingly wordy, but it basically breaks down to this for our snake game; by using our dispatch, a function that never changes, inside of our useEffect callback, we ensure our useEffect callback is only called once. This is what allows us to avoid resetting our interval and get a smooth movement for our snek 🐍. We’ll step through this obviously, but if you want to see a quick example in action, check this out.

For those of you familiar with redux and reducers — that knowledge ain’t gonna help here. We’re going to use our dispatch and reducer in a very unconventional manner. In our switch , we’re going to check the value of direction instead of action.type , and update our position state based on that. In fact, we aren’t even going to pass an object containing a type or payload into dispatch . Because our reducer is only only going to modify our position state in one way, and no payload is necessary, we can assume the update to the state will be of the same pattern each time.

Ok, so no action is passed along, and we switch on a value from useState . Weird af but ok. Let’s check out how to start this.

Let’s start on line 7. We invoke our newly imported useReducer , passing in the function declaration reducer as our first argument, and [0, 0] as the second. The first arg is obviously the reducer we want to call when invoking dispatch , and the second arg is the initial value of our state, much like the value we pass into useState .

Let’s examine our reducer function. It takes two args — the first is the current state, which we destructure to get the x and y values, and the second is usually the action , but like I mentioned, because we really only have one type of action, and never any payload, we’re going to skip over this argument. Bear with me. Take a look at the switch statement. We check against the value of direction , and return an array that represents the updated state. This is much like the set of logic we applied inside our useEffect callback earlier. Most notably, the direction value inside of the our reducer definition will always be fresh, so we can be confident our user input will be accurately output.

The next step is to refactor our useEffect callback to use dispatch . This is pretty nice, since we just wipe out pretty much all of our existing logic and replace it with a one liner.

Yup.

We just call dispatch every 1000 milliseconds to run our reducer, update our position , and rerender the component. Because the definition of dispatch remains the same throughout the lifetime of the App component, this effect will only ever be run once, and it’s cleanup will only ever be called once. This means our interval will not be interrupted and the snake will move smoothly. You might have noticed we put dispatch as one of our deps in our array. dispatch will never change, but Dan Abramov recommends putting any outside dependencies into the deps array, just to be explicit.

🚂🚋🚋🚋

Final Words

We’re not going to build out the full snake game. Though it would be fun, that misses the point. But I want to take a moment to talk about the useReducer approach we took for solving our interval issue; it’s pretty weird. We could have moved direction into the state produced by useReducer , but that only makes our reducer look more conventional. It still doesn’t address the issue that we need to use dispatch to solve our useEffect interval issue.

The docs state we should use useReducer when we have complex state logic that involves multiple sub-values or when the next state depends on the previous one. Well, I wouldn’t consider our state logic very complex, though I guess our next state does depend on previous. But like I stated above, omitted from suggested use cases is our use case —where don’t want to run an effect more than once. It makes we wonder if use case was originally intended or just a lucky coincidence. Probably the former, but the fact that it’s not in the docs and that you’d have to do a bit of deeper digging around blog articles for this is a bit puzzling. Even if it was more explicit, I dare say the component lifecycle approach feels more straightforward. Set up the interval in componentDidMount , and remove it in componentDidUnmount . Bam.

This isn’t to say I don’t like hooks. Quite the opposite. But when using intervals in this manner, it’s something we as react devs will have to be aware of and will have to work to make the knowledge commonplace. Thanks for reading!

🐈

📝 Read this story later in Journal.

👩‍💻 Wake up every Sunday morning to the week’s most noteworthy stories in Tech waiting in your inbox. Read the Noteworthy in Tech newsletter.

--

--