I recently had a chance to work with draggable HTML elements and I’d like to share some of my experience by creating a shopping list that will be controlled only by drag and drop. In order to do that, we will be using react-dnd. The first exposure to this library might be a little intimidating, but once you get how it works it’ll be a pleasant experience. To find out more about this library I highly recommend you to check out this documentation
In this article, we’re going to touch a little on the theory, but our goal is to create a functional example from the scratch, which is why it’s divided into two parts. The first portion concerns the frontend and the backend structure included in the starter. To create the starter, we used the create-react-app script and formed a simple frontend layer to save some time on building components. In addition to the frontend, we mocked a REST API backend with Mirage JS. We chose only the newest technologies such as react-query, logic based on custom hooks, and it’s all in the Typescript 🚀🚀🚀 The second part of this article concerns the drag and drop. We will go step by step through the process of connecting react-dnd to your application 😄
####Final version
Below is a gif of the final version that we will be building in the article. You can play with the table here: Demo
Project setup
A starter branch for this tutorial is available on GitHub
##Backend structure This chapter is mainly to let you know how the backend works. If you don’t want to go through the Backend and Frontend structures, you can go straight to the drag and drop part
As I mentioned before, the app uses Mirage JS as the mocked server; it allows creating every CRUD method with a specified endpoint, designing a database schema, and much more. You can even set up GraphQL API if you like.
First, let’s go through the endpoints and server methods. For the sake of simplicity, the whole server logic is contained in just one file.
The server file creates a database schema based on the objects we’ve passed to the server.db.loadData, and initializes route handlers. Route handlers take the route as the first parameter, and the function that handles your route as the second parameter. Function route handlers can take two arguments schema which allows for access to the mirage JS data layer and request which enables the definition of dynamic routes by using colon syntax (:id in your route, for example), or access body inside request in request.requestBody object.
The server starts with react app. To make this possible, you need to include createMirageServer on top of your app.
src/index.tsx
import {createMirageServer} from './server/server'
createMirageServer()
####Data fetching Data fetching will be done by using axios with react-query to handle requests and cache their result. In some cases, cached data will also be the source of data in this app. To save the query result in the cache and to interact with it, you need to wrap the app with QueryClientProvider and pass a queryClient.
index.tsx
import {QueryCache, QueryClient, QueryClientProvider} from 'react-query'
const queryCache = new QueryCache()
const queryClient = new QueryClient({queryCache})
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
After this step, you can use one of the react-query functionalities interacting with a cache. The data is stored in the cache under the key that was provided to useQuery. For easier maintenance of queries, queryKeys are stored in a separate file. This way, you can simply import the queryKey you need while avoiding typos.
The useQuery requires at least a key for the query (queryKey in this case), and a promise that resolves the data or throws error. You can either pass a cache key, promise, and config object to useQuery in this exact order or use object syntax with at least queryKey and queryFn provided.
Key, function, config object
import axios from 'axios'
import {useQuery} from 'react-query'
import {routes} from '../../server/server'
import useQueryCache from './useQueryCache'
import {FoldersList} from '../../utils/types'
const getFolders = async () => {
const {data} = await axios.get(routes.folders)
return data
}
const useGetFolders = () => {
const {
queryKeys: {folders},
} = useQueryCache()
return useQuery<FoldersList[]>(folders, getFolders)
}
The last thing you need to know is how to access your cached data.
Since react-query v3 you have to use useQueryClient, with this hook you can use the getQueryData method and pass one of the queryKeys to this function.
The file collects all the CRUD methods used in this app. Each method is separated from others; this way you can avoid most of the unexpected behavior, and it makes maintenance and extending functionality much easier.
Here’s an example of using the useApi hook.
const {
folders: { data },
} = useApi()
####Data structure
As we use Typescript, we specify types for the data.
Id - unique value for each product/folder/productInFolder
name - name to display on frontend
order - positive number, fetched data is sorted by this value before it’s displayed
folderId - only for ProductsInFolder, this value is the id of the folder in which the product was dropped.
####Context
In this app, we created a context Main, which provides a state with a currently active folder, and a method to set the active folder.
To use the context, there is a file for the provider, context, and a custom hook to consume it.
src/Contexts/MainContext.tsx
import {createContext} from 'react'
import {MainContextType} from '../Providers/MainProvider'
export const MainContext = createContext<MainContextType | null>(null)
Having such a context setup, you can access values passed to the provider by using the useMain hook if a file is inside this context scope.
const {activeFolder} = useMain()
##Frontend structure
App layout:
Header - responsible for displaying a list of the fetched products;
Main - contains a box with two sides;
LeftSide - displays the list of the fetched folders;
RightSide - displays the list of the products with folderId equal to the selected folder id;
ProductListItem is used in the RightSide component as a folder and the LeftSide component as a product. The difference is the passed type and if it’s the product it also takes a folderId.
The above components should be your main focus. Later in the drag and drop part you will see how to connect all the dnd functionalities on them.
Components are styled with Tailwind CSS. Don’t worry if you’re not familiar with this library, it’s quite easy to understand, and there hasn’t been much styling done in it. In short, these are predefined classes.
##Drag and drop methods
####Introduction
While the previous section was mainly to let you know how the whole app works, in this part we will proceed with the actual code.
React-dnd uses items, types, and monitors to help you with work on your drag and drop app.
Item with type is a plain JavaScript object with the type as a string defined inside it. It allows for describing a dragged element with whatever data you need and keeping draggable components separated from others.
Monitors lets you update the props of your components in response to the drag and drop state changes. You can pass monitor functions to the component by using a collection function that can collect any methods which you might need.
Every component that you’d like to make draggable/droppable has to be marked as a drag or a drop source. What it does is tying the types, the items, the side effects, and the collecting functions together with your component. Drag source defines an item object with the declared type or optionally some extra methods that you might need for a given component. Drop target does not create an item; instead, it can accept many types of items and handle its drop or hover. Another thing about react-dnd is the backend that you provide. React DnD uses the HTML5 drag and drop API. The reason why it’s a good choice is that you don’t have to do any drawing when the cursor moves. That said, the downside of this backend is that it doesn’t support touch events. This library ships with HTML5Backend, but you’re not obligated to use this specific one. If you want, you can use any other backend such as Touch backend for mobile web applications.
####Provider The first thing you need to do is make sure your App or a part of it is wrapped with Dnd context provider and has initialized backend. For this project, we will be using HTML5Backend, which comes by default with this library.
Because it’s built on top of the HTML 5 drag and drop API this example won’t work on mobile devices.
Our first step will be to create a custom hook for each dnd method to make this code a little cleaner.
Let’s start with the first one, which will be responsible for making elements draggable.
This is the most basic drag method, which includes just the elements required by react-dnd. An item is an object that will be passed to other methods providing info about the dragged element. In this app, the item type can be a ‘folder’ or a ‘product’. Therefore, in the case of the useDragMethod hook, we have to pass the type parameter to be able to recognize whether we’re dragging a folder or a product.
This is the most basic drop method. All that react-dnd requires for the drop method is pointing what types of item object are accepted for this drop. You can point just one or you can indicate many by passing types in an array. Of course, making it functional requires adding a little more code.
const itemRef = useRef<HTMLDivElement>(null) //creating react ref
const {drop} = useDropMethod() //getting drop function from custom hook
drop(itemRef) //connecting drop method to referenced element
return (
<div className={`w-full`} ref={itemRef}>
This way we made every ProductListItem draggable and set LeftSide as a drop zone. When triggered by the drop, it will console log an item object defined in the useDragMethod hook.
Now that we’re sure everything is working fine we can continue to expand the functionality. Let’s start with adding more information about the dragged elements such as id, name, and folderId if it’s a product because we need to know if it’s already in a folder.
This way, we got some useful information. Let’s add more drop zones and pass a proper location, so that the functionality for each drop zone can be different.
Note that the Main component is over LeftSide and RightSide, so it also triggers even if you’ve dropped your element on one of these sides. To prevent such behavior, we can check if a drop event has already happened. In order to do that, we can use the 2nd parameter of the drop function monitor, which can give us plenty of useful information such as if an item has in fact dropped.
Once we’re sure that everything is up and running, we can start connecting our API functionality to the drop method. Let’s begin with deleting folders and products when they are dropped in the main drop zone.
####Change the product folder or add a new product
Next, let’s add an option to change the product folder or add a new product to the selected folder. To do that, we need to add another parameter for the useDropMethod hook, i.e. optional targetId, which will be an id of the currently selected folder if it’s on the RightSide or the id of the folder label that we’re dropping on the LeftSide. To make it happen, we need to connect the drop method to ProductListItem and connect the drag method to products in the upper list to make them functional as well.
Let’s start with updating the products in the header by making them draggable.
This way, if you drop a product on the RightSide component with an active folder, or a folder, it will pass a targetId, which will be equal to folder id. The last step is to connect API mutations with the new parameter.
src/Hooks/DragAndDrop/useDropMethod.tsx
const {
deleteProductFromFolder,
deleteFolder,
deleteProductFromUpperList,
putProductsInFolder,
postProductInFolder,
} = useApi()
...
switch (type) {
case folder:
... //previous code
case product:
... //previous code
if (targetId === undefined || folderId === targetId) return
if (folderId !== undefined) {
//update products that are already in other folder
putProductsInFolder({
id,
folderId,
targetFolderId: targetId,
})
} else {
//executes for products from the upper list
postProductInFolder({targetId, name})
}
break
}
The above code checks if the targetId exists and if the current product folderId is the same as the targetId. If any of these cases are true, the method will stop. In any other case, it checks if the product is already in the folder and makes a PUT call to update the folderId for the dragged item or POST call to add a dragged element from the upper list.
Lastly, we’re going to return a drop location. If we return a plain javascript object inside the drop method, it will be available inside the monitor.getDropResult() method. This is how the final version of the useDropMethod.tsx hook looks like.
To change the sequence of the elements, we need their current order. In this case, it will be an index from the array which they were mapped from. We need to update the components to receive an index and pass it to the drag hook.
In the next step, we will create a few conditions to check if the action after hovering should trigger. This is necessary because it could activate too many times, sooner or later making the app unusable.
The exported function that will gather all the conditions is canMove.
These conditions check if: the ref is connected to the HTML element, an item is dragged in the correct zone (basicCheck), we’ve dragged over half of the other element, and lastly if dragging takes place upward or downward.
####Finishing touches on useHover hook
After implementing all the necessary conditions, we can go on to create more functionality.
This is the final version of the hover method. Action that triggers if every condition above is true checks the queryKey based on location, then it gets data from the cache saved under the queryKey and gets all the information about the dragged item based on its index. Then, it swaps the position of the dragged item with the item that was dragged over and their indexes, and at the end sets new order in the cache to make changes visible for the user.
####Update order with backend call
Pretty much the last thing we need to do is connect this action to our backend. This way, the order in our app will remain persistent. For this purpose, we will use another method of useDrag hook - end. It will trigger right after dragging is over.
This method checks if the item type is a folder or a product, and triggers a different call for each one. If it’s the folder, we simply need to get a previously updated list from the cache and send it to the backend to save the order. Basically the same goes for the product, the only difference being that it checks if an item contains folderId and sends one more parameter in a payload to our backend call.
Finishing this step makes the app fully functional.
We have reached the end of this article. If you feel like it, you can keep playing with this repository, e.g. by changing the preview of the dragged element or adding some styles. React-dnd is a powerful library that offers much more than what was shown in this guide.
I hope that you learned something about drag and drop today 😄
If you want to play with this project, you can clone this repository from GitHub.
Share our work
React
React Query
Mirage JS
Typescript
Written By
Chris Cisek
See related posts
Category
September 14, 2020
Pimp your VS Code
Developer productivity
When I switched from Webstorm to VS Code, I wanted to make it as similar as possible. During the time, I noticed that doesn't make sense. VS Code has a much more robust workflow, it just needs a few changes, about which you will read in this article.
React Table 7 - hooks based library to create beautiful, custom tables in React. Build a fully-fledged table step by step with us, implement the most common features like sorting, filtering, pagination and more. Learn how v7 is different from v6.
By clicking “Accept all”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.