Drag and Drop user interfaces are very common in modern applications and yet it’s quite difficult to get them right. Using the HTML Drag and Drop API can be a pain, and it’s known to have many inconsistencies and gotchas. Making a complex Drag and Drop UI in React is not an easy task either, it can get overwhelming pretty quickly. But, libraries like react-dnd and react-beautiful-dnd are there to help us. I am particularly fond of react-beautiful-dnd because of its simple API, beautiful (as the name already suggests) movement animations and baked-in accessibility features. Today, we will take a closer look at using react-beautiful-dnd by making a simple yet fun game.
Here's a demo of the game we'll be creating and a quick video of how it works below.
We will create this simple game, where we will have three List-like components one for Marvel heroes, one for DC heroes and the third one called the “Bench” for holding all the heroes that are yet to be placed in the correct List. To make it more interesting, the scoring will be done on not just the correct grouping of the heroes but also on sorting them alphabetically and finishing as quickly as possible.
We will start by bootstrapping a React project using the amazing create-react-app (v2) or creating a new React project on Codesandbox. We will install our dependencies which are react, react-dom and react-beautiful-dnd. Finally, we’ll also bring in spectre.css to help us make the app look decent with minimal effort.
Let’s look at the data that our game will be using:
So, we have two comics and twelve heroes (six from each faction). Now, let’s look at the
initialState of our app. The initial state has three Array fields - each one holding the data for the List we discussed earlier. We start with all the heroes in the “Bench”. To make things randomized, we shuffle the entry order by using a simple Knuth Shuffle algorithm. We also set the
gameState to “ready” as in ready to start. The other two possible game states are “playing” and “done”.
In the shuffle algorithm, we start from the end of the array and replace it with a random indexed item on the array, and we keep doing this until we reach the first element of the array.
Drag and Drop
Now, let’s look at the structure of the main
App component and specifically the render function first. Then, we will dive into the logic bits shortly after. The
<> syntax is a convenient shorthand for React.Fragments which let’s you return multiple adjacent JSX elements without wrapping them up in a wrapper/container element. We are using five components here -
Modal component help manage different game states, while the
Footer component just renders some static text content. The
Modal component is only rendered when the game is in a “ready” or “done” state.
DragDropContext component is provided by the react-beautiful-dnd and is used to wrap any part of the React tree that needs to support the Drag and Drop functionality. It is usually advised to have only one of these components wrapping your entire React app and not have nested
DragDropContext components. It is similar to the
Provider component pattern, you might be familiar with when using Redux. In our case, this subtree is only rendered when the game state is either “playing” or “done”.
Dropzone components are the children of the
DragDropContext component because that’s where we need our Drag and Drop feature. You can notice we have three
Dropzone components, one for each List type with their respective id and heroes passed as props to them.
react-beautiful-dnd exports two other major components that leverage the Render props pattern -
Droppable component is used for enabling Drop functionality while the
Draggable component is used to enable Dragging functionality. The
Draggable component child can be dropped on a
Droppable component child. Therefore, each
Dropzone component renders a
Droppable list-like component with
draggableId set to the id props passed to it. The
draggableId works as a unique identifier for that
Droppable and can be used for various advanced usage like allowing conditional logic based drop capability and more. We also use a
isDropDisabled prop to disallow accepting drop elements when the game has not started or has ended already. Finally, we map over all the heroes currently in the list and render a
Draggable component for each. So, now we have three lists that render various Superheroes, that we can drag and drop onto other lists. But, the Drag and Drop is just dummy functionality, for now, the dragged Superheroes don’t persist in the Lists they were dropped onto. For that, we need to wire up the logic to update the state accordingly.
To handle state changes related to Drag and Drop, we use the
onDragEnd props on the
DragDropContext component. When a
Draggable is dropped on a
Droppable component inside the tree under
DragDropContext, the method passed to
onDragEnd props is called. There are other optional methods too but this is the only required one. We can access the source and destination
Droppable for the interaction and accordingly update our state. If the destination is "falsy, then we don’t update the state because the Drop was invalid, otherwise we pass the current app state, source and destination value to the move function exported from utils.js.
In the move function, we make a copy of the source list’s state. We also make a copy of the destination list’s state if the source and the destination are different lists, otherwise, use the source list copy as the destination list copy. Using the
Array.splice method we remove the Dragged element from its current index in the source list and then add it to the destination list at an index which will reflect the position where it was Dropped. We then return the updated lists to be used for updating the
App component state. Now, our application has a functioning Drag and Drop feature i.e. the core mechanics of our game. We can move onto adding the final features like score calculation, timer, etc.
Start, Loop and Reset
Next, let’s looks at the logic behind the startGame, endGame, resetGame and gameLoop.
The startGame method updates the state with
gameState field set to “playing” and the
currentDeadline instance value to 30 seconds later. It also invokes the
gameLoop method following the state update by passing it as the second callback to
gameLoop starts a setInterval that updates the timeLeft state value every second (a crude game loop implementation). When the time is up, it sets the
timeLeft to zero and
gameState to “done” and also clears the timer. We also add a
componentWillUnmount lifecycle hook to cleanup any uncleared timers to avoid potential memory leaks.
The game also provides a button on the
Header component to end the game early to score bonus points. The endGame method handles that case by setting the
gameState to “done” and clearing the timer. In this case, we do not make the
timeLeft value zero because we will use it to calculate the score later. Finally, we have a resetGame method which simply resets to the
Before, we can move onto the final feature of our game let’s look at the Components that still need to be discussed. The
Footer element just renders a link to the icon set we are using for the Superheroes elements.
Header element is passed the
gameState and the amount of
timeLeft to complete the current game round as props and it renders the number of seconds left until a game round ends when the game is in “playing” state. It also renders a button to end game early to score bonus points.
Modal component handles the other two game states - “ready” and “done”. When the game is in “ready” state, the
Modal component renders a welcome text and a button to start the game. When the game is in “done” state, it renders the score achieved and a button to reset the game.
The score displayed on the
Modal component at the end of a game round is calculated by the
calculateScore methods in the utils.js.
calculateScore method takes either of the Marvel or DC grouped and sorted heroes lists and compares them against the appropriate ideal list order. If a hero is present in the correct list and the correct index, full points are awarded for that hero i.e. the total number of heroes (12 in this case). If the hero is present in the correct list but the sort order is incorrect, the difference between the correct and incorrect index is subtracted from the points to be awarded. If the hero is not present in the correct list, zero points are awarded. Finally, the total score for the two lists are added along with any time bonus (1 point for each second left) in the getTotalScore method and returned as the total score.
We wrap up by mounting our
App component in index.js and importing our CSS to make the application look nice and clean.
If you're interested in playing with the source code for this project you can find that here here.
Drag and Drop implementations are still quite complicated but a crucial part of the modern web. Libraries like react-beautiful-dnd make it approachable and fun to develop. There is a lot more you can do with this package, and their documentation is all you need to dive deeper. If you make something great with it, they have an issue where you can share your applications and experiments for everyone to enjoy. If you find a use case where react-beautiful-dnd isn’t customizable enough, you can try out react-dnd which is great too and is made for broader use cases. I hope you have fun playing this game (try playing it with just your keyboard) and building your own.