This content originally appeared on Level Up Coding - Medium and was authored by Itsuki
Can you imagine a sortable table that requires you to press on those up and down buttons for 10 thousands times to move a row from bottom to top?
I know services / web apps that do this and I hated it!
So!
In this article, let’s check out how we can create a table that allows us to re-order rows by drag and drop! From scratch!
We will be using React and Typescript here but similar(Not same unfortunately) ideas apply to regular HTML/Javascript as well!
Set Up
A simple Pokemon Table to display the Index No. and the name.
import React from "react"
export type Pokemon = {
id: number
name: string
}
const initailPokemons: Pokemon[] = [
{ id: 1, name: "bulbasaur" },
{ id: 2, name: "ivysaur" },
{ id: 3, name: "venusaur" },
{ id: 4, name: "charmander" },
{ id: 5, name: "charmeleon" },
{ id: 6, name: "charizard" },
{ id: 7, name: "squirtle" },
{ id: 8, name: "blastoise" },
{ id: 9, name: "caterpie" },
{ id: 10, name: "metapod" },
]
export default function Demo() {
const [pokemons, setPokemons] = React.useState<Pokemon[]>(initailPokemons)
return (
<div className="flex flex-col gap-4 w-48 m-20">
<h1 className="text-xl font-bold">Pokemons</h1>
<table className="border-collapse border-slate-500 border-2">
<thead>
<tr>
<th className="border-2 border-slate-600 p-1">No.</th>
<th className="border-2 border-slate-600 p-1">Name</th>
</tr>
</thead>
<tbody>
{pokemons.map((pokemon, index) => (
<tr key={pokemon.id}>
<td className="border-2 border-slate-700 text-center p-1 cursor-default">{pokemon.id}</td>
<td className="border-2 border-slate-700 text-center p-1 cursor-default">{pokemon.name}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
(Why Pikachu is №25….I am too lazy to type 25 entries so I will leave it out for today…)
I am using tailwind for styling here but obviously it is not going to effect the drag and drop logic at all what so ever!
Draggable
Similar to what I have shared with you previously on how we can Support Drag and Drop for File Upload, we will be again using the Drag and Drop API here, but with some different events.
First of all, we will need to make a table row draggable by adding
<tr key={pokemon.id}
draggable
onDragStart={(e) => dragStart(e, index)}
>
Now, what should we do in dragStart function?
We need to keep a reference of which row is being dragged.
There are couple ways of doing this. If we are in a non-react world, we can set it using the dataTransfer.setData method like following and retrieve the data later with getData.
function dragStart(e: React.DragEvent<HTMLTableRowElement>, index: number) {
e.dataTransfer.setData("application/json", JSON.stringify(pokemons[index]))
}
But!
Let’s create a state variable for this because it is a lot easier to work with and there are some other places that we will be using it later!
const [draggingIndex, setDraggingIndex] = React.useState<number | null>(null)
We can then set it on dragStart.
function dragStart(_e: React.DragEvent<HTMLTableRowElement>, index: number) {
setDraggingIndex(index)
}
Yeah! We are able to drag our rows!
Before we move onto dropping those, I would just like to share with you couple options we can configure on the dragging behavior really quick.
Customize Dragging
First of all, by default, on dragStart, a translucent image is automatically generated from the drag target (the element the dragStart event is fired at) and appears beside the pointer during the drag operation.
If we want to use a custom image instead, we can set it with the setDragImage method like following.
const img = new Image();
img.src = "https://i.pinimg.com/736x/d1/23/5d/d1235dba2efc668cf4bd3c6bf82c00cd.jpg";
e.dataTransfer.setDragImage(img, 10, 10);
The source here can be either a remote URL or a local file.
Also, it is not working in Safari! At least not for me! Chrome works fine though…
MDN doc claims it does. But nope…
We can also control which cursor the browser displays during the drag operation with the dropEffect property.
Three effects are available.
- copy indicates that the dragged data will be copied from its present location to the drop location.
- move indicates that the dragged data will be moved from its present location to the drop location.
- link indicates that some form of relationship or connection will be created between the source and drop locations.
We can set it like the following.
e.dataTransfer.dropEffect = "move";
Drop Destination
To make a row drop destination, we will be defining the dragEnter event.
<tr key={pokemon.id}
draggable
onDragStart={(e) => dragStart(e, index)}
onDragEnter={(e) => dragEnter(e, index)}
>
and within the event, we will be updating both the pokemons as well as the draggingIndex.
function dragEnter(e: React.DragEvent<HTMLTableRowElement>, index: number) {
if (draggingIndex === null || index === draggingIndex) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setPokemons((prevState) => {
let newPokemons = [...prevState];
const deleteElement = newPokemons.splice(draggingIndex, 1)[0];
newPokemons.splice(index, 0, deleteElement);
return newPokemons;
});
setDraggingIndex(index);
}
Note that I am calling preventDefault here to prevent additional event processing for this event such as touch events or pointer events.
I know, as you can see, when we finish or cancel the drag and drop operation, we are getting the going back effect on the row being dragged which is not really desirable. We will be solving this in couple seconds, but before that, let’s add the dragEnd really quick.
Drag End
dragEnd is fired when a drag operation ends, by either releasing a mouse button or hitting the escape key.
This is a great place to reset the states, saved the data by posting to the server, and etc.
<tr key={pokemon.id}
//...
onDragEnd={(e) => dragEnd(e)}
>
We will only be resetting the dragIndex back to null here.
function dragEnd(e: React.DragEvent<HTMLTableRowElement>) {
setDraggingIndex(null)
}
Remove The Going-Back-Effect
There are actually two cases we need to think about here.
First of all, let’s handle the case where the drag operation finished with success. To remove the effect, all we have to do is to preventDefault on dragOver.
<tr key={pokemon.id}
draggable
onDragStart={(e) => dragStart(e, index)}
onDragEnter={(e) => dragEnter(e, index)}
onDragOver={(e) => e.preventDefault()}
onDragEnd={(e) => dragEnd(e)}
>
However, this will only prevent the effect on drags actually performed (finished with success) but not if the drag was cancelled.
This is because we are SUPPOSED TO set the data back to its original state if the drag is cancelled!
So!
Instead of trying to remove the behavior in this case, let’s improve our implementation to reset the data back to the state before the operation.
There are two ways of approaching this!
- Instead of modifying the data on dragEnter, use dragOver + drop and only modify the data on drop
- Keep a reference to the previous data and set it back if the drag operation is cancelled
I will choose the second approach here because I do like the resorting behavior when we drag over a drop area.
To tell if a drag and drop operation is actually finished or cancelled, we can use the onEnd we have above by checking the dropEffect property. If it has the value none during a dragend, then the drag was cancelled. Otherwise, the effect specifies which operation was performed.
We will be adding another state variable here to keep track of the state before drag starts and update it accordingly on each drag event handler.
const [previousPokemons, setPreviousPokemons] = React.useState<Pokemon[]>(initailPokemons)
function dragStart(e: React.DragEvent<HTMLTableRowElement>, index: number) {
e.dataTransfer.dropEffect = "move";
setDraggingIndex(index)
setPreviousPokemons([...pokemons])
}
function dragEnter(e: React.DragEvent<HTMLTableRowElement>, index: number) {
if (draggingIndex === null || index === draggingIndex) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setPokemons((prevState) => {
let newPokemons = prevState;
const deleteElement = newPokemons.splice(draggingIndex, 1)[0];
newPokemons.splice(index, 0, deleteElement);
return newPokemons;
});
setDraggingIndex(index);
}
function dragEnd(e: React.DragEvent<HTMLTableRowElement>) {
setDraggingIndex(null)
if (e.dataTransfer.dropEffect === "none") {
setPokemons([...previousPokemons])
} else {
setPreviousPokemons([...pokemons])
}
}
As you can see, if we decide to cancel the event, the row moves back to its original place.
Little Note
Always make sure to create a new array when assigning the state instead of using the state variable from another state, ie: setPokemons([…previousPokemons]), setPreviousPokemons([…pokemons]), so that the pokemons and previousPokemons are not pointing at the same object.
Little Discussion
You might be wondering why don’t I resort on dragOver instead of dragEnter.
The behavior might look similar. However, if we log out both events, we will see that
- the dragEnter event fires the moment we drag in to the target area for only once
whereas
- the dragOver event fires during the time we are dragging within the drop area until we leave the area or drop it, every few hundred milliseconds
Not Needed in this case!
A Little Extra Style
To finish up our day, let’s add a little styling to the row being dragged so that it can stand out!
It is simple, all we have to do is to check whether the draggingIndex is the same as row Index and that’s why we have used a state variable instead of simply set the data on the dataTransfer!
For example, A little different background!
<tr key={pokemon.id}
//...
className={index === draggingIndex ? "bg-default" : ""}
>
Wrap Up
Code for today!
'use client'
import React from "react"
export type Pokemon = {
id: number
name: string
}
const initailPokemons: Pokemon[] = [
{ id: 1, name: "bulbasaur" },
{ id: 2, name: "ivysaur" },
{ id: 3, name: "venusaur" },
{ id: 4, name: "charmander" },
{ id: 5, name: "charmeleon" },
{ id: 6, name: "charizard" },
{ id: 7, name: "squirtle" },
{ id: 8, name: "blastoise" },
{ id: 9, name: "caterpie" },
{ id: 10, name: "metapod" },
] as const
export default function Demo() {
const [pokemons, setPokemons] = React.useState<Pokemon[]>(initailPokemons)
const [draggingIndex, setDraggingIndex] = React.useState<number | null>(null)
const [previousPokemons, setPreviousPokemons] = React.useState<Pokemon[]>(initailPokemons)
function dragStart(e: React.DragEvent<HTMLTableRowElement>, index: number) {
e.dataTransfer.dropEffect = "move";
setDraggingIndex(index)
setPreviousPokemons([...pokemons])
}
function dragEnter(e: React.DragEvent<HTMLTableRowElement>, index: number) {
if (draggingIndex === null || index === draggingIndex) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setPokemons((prevState) => {
let newPokemons = prevState;
const deleteElement = newPokemons.splice(draggingIndex, 1)[0];
newPokemons.splice(index, 0, deleteElement);
return newPokemons;
});
setDraggingIndex(index);
}
function dragEnd(e: React.DragEvent<HTMLTableRowElement>) {
setDraggingIndex(null)
if (e.dataTransfer.dropEffect === "none") {
setPokemons([...previousPokemons])
} else {
setPreviousPokemons([...pokemons])
}
}
return (
<div className="flex flex-col gap-4 w-48 m-20">
<h1 className="text-xl font-bold">Pokemons</h1>
<table className="border-collapse border-slate-500 border-2">
<thead>
<tr>
<th className="border-2 border-slate-600 p-1">No.</th>
<th className="border-2 border-slate-600 p-1">Name</th>
</tr>
</thead>
<tbody>
{pokemons.map((pokemon, index) => (
<tr key={pokemon.id}
draggable
onDragStart={(e) => dragStart(e, index)}
onDragEnter={(e) => dragEnter(e, index)}
onDragOver={(e) => e.preventDefault()}
onDragEnd={(e) => dragEnd(e)}
className={index === draggingIndex ? "bg-default" : ""}
>
<td className="border-2 border-slate-700 text-center p-1 cursor-default">{pokemon.id}</td>
<td className="border-2 border-slate-700 text-center p-1 cursor-default">{pokemon.name}</td>
</tr>
))}
</tbody>
</table>
</div >
);
}
Thank you for reading!
That’s it for today!
I know! Drag and Drop is always the best!
(Please don’t scream at me if you disagree!)
React/Typescript: Table Row Re-Ordering with Drag and Drop was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Itsuki
Itsuki | Sciencx (2025-01-10T02:25:23+00:00) React/Typescript: Table Row Re-Ordering with Drag and Drop. Retrieved from https://www.scien.cx/2025/01/10/react-typescript-table-row-re-ordering-with-drag-and-drop/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.