Today you will learn how to build your own clone of the 2048 game in React.
What makes this article unique is that we will focus on creating delightful animations. Aside from React, we will use TypeScript and we'll make some CSS transitions using LESS.
We are only going to use modern React interfaces such as hooks and the Context API.
This article contains a few external resources such as:
...and a YouTube video. It took me more than a month to prepare this tutorial, so it would mean the world to me if you watch it, smash the like button, and subscribe to my channel.
Thank you!

2048 Game Rules

In this game, the player must combine tiles containing the same numbers until they reach the number 2048. The tiles can contain only integer values starting from 2, and that are a power of two, like 2, 4, 8, 16, 32, and so on.
Ideally, the player should reach the 2048 tile within the smallest number of steps. The board has dimension of 4 x 4 tiles, so that it can fit up to 16 tiles. If the board is full, and there is no possible move to make like merging tiles together - the game is over.
While creating this tutorial I took shortcuts to focus on the game mechanics and animations. What did I get rid of?
If you want you can implement those missing features on your own. Just fork my repository and implement it on your own setup.

The Project Structure

The application contains the following elements:

How to Build the Tile Component

We want to invest more time in animations, so I will start the story from the Tile component. In the end, this component is responsible for all animations in the game.
There are only two fairly simple animations in 2048 – tile highlighting, and sliding it across the board. We can handle those animations with CSS transitions by declaring the following styles:
.tile {
  // ...
  transition-property: transform;
  transition-duration: 100ms;
  transform: scale(1);
}
At the current moment I defined only one transition that will highlight the tile when it is created or merged. We will leave it like that for now.
Let's consider how the Tile meta data is supposed to look, so we can easily use it. I decided to call it 
TileMeta
 since we don't want to have the name conflict with other entities such as the Tile component:
type TileMeta = {
  id: number;
  position: [number, number];
  value: number;
  mergeWith?: number;
};

How to Create and Merge Tiles

We want to somehow highlight that the tile changed after the player's action. I think the best way would be changing the tile's scale to indicate that a new tile has been created or one has been changed.
export const Tile = ({ value, position }: Props) => {
  const [scale, setScale] = useState(1);

  const prevValue = usePrevProps<number>(value);

  const isNew = prevCoords === undefined;
  const hasChanged = prevValue !== value;
  const shallAnimate = isNew || hasChanged;

  useEffect(() => {
    if (shallAnimate) {
      setScale(1.1);
      setTimeout(() => setScale(1), 100);
    }
  }, [shallAnimate, scale]);

  const style = {
    transform: `scale(${scale})`,
  };

  return (
    <div className={`tile tile-${value}`} style={style}>
      {value}
    </div>
  );
};
To trigger the animation, we need to consider two cases:
And the result is the following:
You might've noticed that I'm using a custom hook called 
usePrevProps
. It helps to track the previous values of the component properties (props).
I could use references to retrieve the previous values but it would clutter up my component. I decided to extract it into a standalone hook, so the code is readable, and I can use this hook in other places.
If you want to use it in your project, just copy this snippet:
import { useEffect, useRef } from "react";

/**
 * `usePrevProps` stores the previous value of the prop.
 *
 * @param {K} value
 * @returns {K | undefined}
 */
export const usePrevProps = <K = any>(value: K) => {
  const ref = useRef<K>();

  useEffect(() => {
    ref.current = value;
  });

  return ref.current;
};

How to Slide Tiles Across the Board

The game will look janky without animated sliding of tiles across the board. We can easily create this animation by using CSS transitions.
The most convenient will be to use properties responsible for positioning, such as 
left
 and 
top
. So we need to modify our CSS styles to look like this:
.tile {
  position: absolute;
  // ...
  transition-property: left, top, transform;
  transition-duration: 250ms, 250ms, 100ms;
  transform: scale(1);
}
Once we've declared the styles, we can implement the logic responsible for changing a tile's position on the board.
export const Tile = ({ value, position, zIndex }: Props) => {
  const [boardWidthInPixels, tileCount] = useBoard();
  // ...

  useEffect(() => {
    // ...
  }, [shallAnimate, scale]);

  const positionToPixels = (position: number) => {
    return (position / tileCount) * (boardWidthInPixels as number);
  };

  const style = {
    top: positionToPixels(position[1]),
    left: positionToPixels(position[0]),
    transform: `scale(${scale})`,
    zIndex,
  };

  // ...
};
As you can see, the equation in the 
positionToPixels
 function needs to know the position of the tile, the total amount of tiles per row and column, and the total board length in pixels (width or height – same, it is a square). The calculated value is passed down to the HTML element as an inline style.
Wait a minute... but what about the 
useBoard
 hook and 
zIndex
 property?