Blog#2::Drag and Drop functionality with React/Redux toolkit, @use-gesture/react and Rails API

In my last post of the Building Drag and Drop Dynamic App series I offered some food for thought to people who are looking to build an app that features movable components. Now it’s time to look at the most challenging and exciting part — the actual dragging of the elements.

As mentioned before the example app was built in React with Redux toolkit and Ruby on Rails. The main focus of the current blog is going to be front-end functionality, but feel free to drop me a message below if you have questions about the backend as well.

There are several packages out there which you can use in order to implement a drag and drop functionality. I chose the @use-gesture/react simply because it was my first attempt and it seemed very versatile and user friendly.

  1. Installation, documentation and import

Visit @use-gesture/react and run npm i @use-gesture/react in your React app console. Unfortunately, the npm landing page doesn’t contain a lot of information or any documentation about the package, but I was able to find everything I needed here:

As stated in the docs @use-gesture/react has two packages — one for React and another one for Vanilla JS. You guessed it right — we will be looking at the React one and specifically the hook that is called useDrag. Once you are more familiar with how this hook works, I strongly encourage you to experiment with other ones as well.

All there is left to do at this step is to import the useDrag hook on top of the component which handles the draggable element, like so:

import { useDrag } from "@use-gesture/react";

2. Setting local state to track changes

In the documentation @use-gesture/react is suggested to be used with the useSpring package because it presumably allows for better functionality. This was also the reason I wanted to write this blog, because there are very few next to none examples on how to use it with useState or Redux. In my specific case I needed the state to be tracked globally and therefore I had to find a way to implement the drag feature using call back function that would dispatch a slice action instead of setting the state locally.

Let’s just do a really quick review of how to set a local state using the useDrag hook and then we will migrate it into out Redux toolkit.

import { useState } from "react"
...
const [elementCoordinates, setElementCoordinates] = useState({x: 0, y:0})

Here we are setting our initial state to an object with key value pairs that represent the x and y coordinates.

Now on the div that hold our draggable element lets set the following attributes: position = relative, top and left coordinates to correspond with our trackable state variables like so:

<div
key={sticker.id}
{...bindElementPosition()}
style={{
display: "block",
position: "relative",
top: elementCoordinates.y,
left: elementCoordinates.x,
}}
>

And lastly we will spread div attributes to include the callback function bindElementPosition which we are about to compose.

const bindElementPosition = useDrag((params)=>{
setElementCoordinates({x: params.offset[0],y: params.offset[1]})
})

And whoa! That’s all. Now our element moves and the state is tracking the x&y coordinate changes.

Our bindElementPosition function is set to a useDrag hook which accepts an argument and returns a call back — in our case the state setter function.

You can see a full list of acceptable arguments in the package docs. We used an “offset” here because offset tracks the distance from the first gesture. Offset is an array of two values: x and y — that’s why to get to them we used offset[0] to get the x coordinate and offset[1] to get to the y coordinate.

3. Transitioning into Redux toolkit

You probably already guessed what needs to happen in order for us to update global state instead of the local one. That’s right — it’s the callback magic!!! Instead of returning a setElementCoordinates we are going to dispatch a Redux toolkit action, like so:

const bindStickerPos = useDrag(({delta}) => {
dispatch(boardActions.setElementCoordinates({coordinates: {x: delta[0],y: delta[1]},
}));
});

You will also have to circle back to your useSelector and grab element coordinates from your slice and set them as your div’s starting points instead of the old local state values.

const coordinateX = useSelector((state) => state.elements.coordinateX);const coordinateY = useSelector((state) => state.elements.coordinateY);

Update your div attributes accordingly like so:

<div
key={sticker.id}
{...bindElementPosition()}
style={{
display: "block",
position: "relative",
top: coordinateY,
left: coordinateX,
}}
>

Depending on your dispatch functionality and how your global state is laid out you will have to play around with the set up and fine tune it to your particular situation.

4. Gesture state attributes variances

The most important thing I would want to bring you attention to is the choice of @use-gesture/ react state attributes that are being passed down into the callback. The usage of these attributes will fully depend on the set up of your draggable component’s state that is tracking the coordinates. Therefore the general understanding of subtle argument differences are game changing.

In my first example I chose to use OFFSET. Offset is always being reset to [0, 0] array after each action, meaning that if your component stays unchanged you state is going to be tracked properly. It doesn’t have a huge difference on how the initial state is being declared — after re-render it will always go back to 0 no matter what. It’s a local state, it’s always being reset and if that’s what you want then you can continue using the offset attribute.

However if you are making API calls or making any changes that cause component re-render, your state will be lost. That’s why useState is not ideal. However, even if you saved your coordinates to the backend and are starting off with the proper positioning, the second you initiate a new gesture movement, the callback will dispatch brand new offset = [0, 0] (remember it always goes back to an array of zeros?) and will cause your element to jump back to a potentially undesired position (x:0, y:0).

This issue can be easily fixed with some quick update — we can use DELTA! Delta is one of the useDrag attributes that tracks the difference between MOVEMENTS (movement is essentially a difference between the last offset and the current offset). There is a lot of math and logic here and you don’t really have to dive too deep into it if you don’t want to. When you start using the package more often these differences become more apparent. My advice is to just console log the params often, review values of all the attributes and play with adding and subtracting them from each other, or simply observe the changes in their values. I found offset, first, delta and dragging the most valuable attributes when starting off learning how to use the @use-gesture/react package. Please refer to the documentation referenced earlier for more detailed info on each one of them.

So, going back to our function, if you add delta value to the current value of your coordinates on every callback dispatch — the result will give you new updated coordinates every time. For example: oldCoordinateX(12) + delta(0.1) = newCoordinateX(12.1). Technically your toolkit’s action job is simply to accept delta value from a dispatched payload, add it to an old state and return a new updated state. Subsequently, your useSelector will pull the new state value from the toolkit slice, set the coordinate attributes in div and the div will move your element to a new position.

And this is pretty much all you have to do to introduce such an amazing an interactive feature into your app! Please feel free to leave questions and comments below.

In the next blog of this series, I will show you how to create a “Download as a JPEG/PNG/PDF” feature with React, useRef and react-component-export-image.

Stay tuned and see you soon!

--

--