Using Sortable Lists in React is fairly easy, in NextJS it's a little bit trickier. Here's how to do it with Beautiful DnD and DnD Kit.
Although there are probably hundreds of sortable list modules available for React, 2 of the biggest are Beautiful DnD and DnD Kit (formerly React DnD).
In this article we will examine both, and although I was going to give my verdict at the end, I'll just say it now: DnD Kit is the clear winner.
Here is a simple demo page where you can see both Beautiful DnD and DnD Kit in action
Beautiful DnD is a very popular module, and it's easy to see why. It's pretty well documented, and it's been around for a long time. It's also very fairly easy to use, it supports both Vertical and Horizontal lists, and you can have multiple columns and drop items between them.
Before I show you the code I used to get it working, I'll first address it's biggest flaws:
Lets take a look at the code:
import { ComponentType, useEffect, useState } from "react"; import dynamic from "next/dynamic"; import { DraggableProvided, DroppableProvided } from "react-beautiful-dnd"; interface DataElement { id: number; content: string; emoji: string; } type List = DataElement[]; const initialItems: List = [ { id: 1, content: "First", emoji: "๐" }, { id: 2, content: "Second", emoji: "๐" }, { id: 3, content: "Third", emoji: "๐" }, { id: 4, content: "Fourth", emoji: "๐"}, { id: 5, content: "Fifth", emoji: "๐" }, { id: 6, content: "Sixth", emoji: "๐" }, { id: 7, content: "Seventh", emoji: "๐" }, { id: 8, content: "Eighth", emoji: "๐" }, { id: 9, content: "Ninth", emoji: "๐" }, { id: 10, content: "Tenth", emoji: "๐" }, { id: 11, content: "Eleventh", emoji: "๐ฅ" }, { id: 12, content: "Twelfth", emoji: "๐ฅญ" }, { id: 13, content: "Thirteenth", emoji: "๐ฅฅ" }, { id: 14, content: "Fourteenth", emoji: "๐ฅ" }, { id: 15, content: "Fifteenth", emoji: "๐ฅฆ" }, { id: 16, content: "Sixteenth", emoji: "๐ฅฌ" }, { id: 17, content: "Seventeenth", emoji: "๐ฅ" }, { id: 18, content: "Eighteenth", emoji: "๐ถ" }, { id: 19, content: "Nineteenth", emoji: "๐ฝ" }, { id: 20, content: "Twentieth", emoji: "๐ฅ" }, ]; // Importing the components dynamically to avoid SSR issues const DragDropContext: ComponentType<any> = dynamic( () => import("react-beautiful-dnd").then((mod) => { return mod.DragDropContext; }), { ssr: false } ) as ComponentType<any>; const Droppable: ComponentType<any> = dynamic( () => import("react-beautiful-dnd").then((mod) => { return mod.Droppable; }), { ssr: false } ) as ComponentType<any>; const Draggable: ComponentType<any> = dynamic( () => import("react-beautiful-dnd").then((mod) => { return mod.Draggable; }), { ssr: false } ) as ComponentType<any>; const ListItem = ({ item }: { item: DataElement }) => { return ( <div className="px-4 py-1 my-2 bg-white rounded-md shadow-md border-zinc-300 border flex justify-start items-center gap-x-4"> <span className="text-4xl">{item.emoji}</span> <span className="text-zinc-400 font-bold"> {item.id} - {item.content} Item... </span> </div> ); }; export default function DragAndDropBeautifulDnD() { const [list, setList] = useState<List>(initialItems); const onDragEnd = async (result: any) => { const { destination, source, draggableId } = result; if (!destination) { return; } if ( destination.droppableId === source.droppableId && destination.index === source.index ) { return; } const newList = Array.from(list); const [removed] = newList.splice(source.index, 1); newList.splice(destination.index, 0, removed); setList(newList); }; return ( <> <DragDropContext onDragEnd={onDragEnd}> <Droppable droppableId="droppable"> {(provided: DroppableProvided) => ( <div {...provided.droppableProps} ref={provided.innerRef} className="max-w-md mx-auto"> {list.map((item, index) => ( <Draggable key={item.id} // Ensure each key is unique draggableId={item.id.toString()} // Use a unique identifier for each draggable item index={index}> {(provided: DraggableProvided) => ( <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} className=""> <ListItem item={item} /> </div> )} </Draggable> ))} {provided.placeholder} </div> )} </Droppable> </DragDropContext> </> ); }
Note to get this working in development mode or locally, you will need to add the following to your next.config.js
file:
reactStrictMode: false
I'm not going to lie, this was a pain to get working, and feels like you have to hack the hell out of it, especially with TypeScript. Also note the dynamic
imports, this is to avoid SSR issues.
DnD Kit allows grids, and for me this is one huge advantage over Beautiful DnD. It also works out of the box (Well almost) with NextJS, and it's much easier to get working with TypeScript. It also works with React Strict Mode, which is a huge plus.
Lets also take a look at the code, I've split this up into 3 files:
โโโ index.tsx โโโ Grid.tsx โโโ SortableItem.tsx โโโ Item.tsx
First up, the index.tsx
file:
import React, { FC, useState, useCallback } from 'react'; import { DndContext, closestCenter, MouseSensor, TouchSensor, DragOverlay, useSensor, useSensors, DragStartEvent, DragEndEvent, } from '@dnd-kit/core'; import { arrayMove, SortableContext, rectSortingStrategy } from '@dnd-kit/sortable'; import Grid from './Grid'; import SortableItem from './SortableItem'; import Item from './Item'; import { DataElement, initialItems } from '../../../pages/blog-demos/react-drag-and-drop-list'; export interface DataElement { id: number; content: string; emoji: string; } export type List = DataElement[]; export const initialItems: List = [ { id: 1, content: "First", emoji: "๐" }, { id: 2, content: "Second", emoji: "๐" }, { id: 3, content: "Third", emoji: "๐" }, { id: 4, content: "Fourth", emoji: "๐"}, { id: 5, content: "Fifth", emoji: "๐" }, { id: 6, content: "Sixth", emoji: "๐" }, { id: 7, content: "Seventh", emoji: "๐" }, { id: 8, content: "Eighth", emoji: "๐" }, { id: 9, content: "Ninth", emoji: "๐" }, { id: 10, content: "Tenth", emoji: "๐" }, { id: 11, content: "Eleventh", emoji: "๐ฅ" }, { id: 12, content: "Twelfth", emoji: "๐ฅญ" }, { id: 13, content: "Thirteenth", emoji: "๐ฅฅ" }, { id: 14, content: "Fourteenth", emoji: "๐ฅ" }, { id: 15, content: "Fifteenth", emoji: "๐ฅฆ" }, { id: 16, content: "Sixteenth", emoji: "๐ฅฌ" }, { id: 17, content: "Seventeenth", emoji: "๐ฅ" }, { id: 18, content: "Eighteenth", emoji: "๐ถ" }, { id: 19, content: "Nineteenth", emoji: "๐ฝ" }, { id: 20, content: "Twentieth", emoji: "๐ฅ" }, ]; const DndKitDragAndDrop: FC = () => { const [ items, setItems ] = useState(initialItems); const [ activeItem, setActiveItem ] = useState<DataElement | null>(null); const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor)); const handleDragStart = useCallback((event: DragStartEvent) => { setActiveItem(items.find((item) => item.id === event.active.id) || null); }, []); const handleDragEnd = useCallback(async (event: DragEndEvent) => { const { active, over } = event; let updatedItems = items; if (active.id !== over?.id) { setItems((items) => { const oldIndex = items.findIndex((item) => item.id === active.id); const newIndex = items.findIndex((item) => item.id === over?.id); updatedItems = arrayMove(items, oldIndex, newIndex); return updatedItems; }); } setActiveItem(null); }, []); const handleDragCancel = useCallback(() => { setActiveItem(null); }, []); return ( <> <DndContext sensors={sensors} collisionDetection={closestCenter} onDragStart={handleDragStart} onDragEnd={handleDragEnd} onDragCancel={handleDragCancel} > <SortableContext items={items} strategy={rectSortingStrategy}> <Grid columns={2}> {items.map((item) => ( <SortableItem key={item.id} item={item} /> ))} </Grid> </SortableContext> <DragOverlay adjustScale style={{ transformOrigin: '0 0 ' }}> {activeItem ? <Item item={activeItem} isDragging /> : null} </DragOverlay> </DndContext> </> ); }; export default ReactDragAndDropListGrid;
Next up let's look as Grid.tsx
:
import React, { FC } from 'react'; type GridProps = { columns: number; children: React.ReactNode; }; const Grid: FC<GridProps> = ({ children, columns }) => { return ( <div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4 max-w-800 mx-auto my-10' > {children} </div> ); }; export default Grid;
Next, the SortableItem.tsx
file:
import React, { FC } from "react"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import Item, { ItemProps } from "./Item"; const SortableItem: FC<ItemProps> = (props) => { const { isDragging, attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: props.item.id }); const style = { transform: CSS.Transform.toString(transform), transition: transition || undefined, }; return ( <Item ref={setNodeRef} style={style} withOpacity={isDragging} {...props} {...attributes} {...listeners} aria-describedby="Draggable Item" /> ); }; export default SortableItem;
And finally the Item.tsx
file:
import clsx from 'clsx'; import React, { forwardRef, HTMLAttributes } from 'react'; import { DataElement } from '.'; type ItemProps = HTMLAttributes<HTMLDivElement> & { item: DataElement; withOpacity?: boolean; isDragging?: boolean; }; const Item = forwardRef<HTMLDivElement, ItemProps>(({ withOpacity, isDragging, style, ...props }, ref) => { return <div ref={ref} className={ clsx( ( isDragging ? 'cursor-grabbing shadow-lg text-zinc-700 font-extrabold' : 'cursor-grab shadow-sm text-zinc-400 font-bold' ) , `bg-white border border-gray-200 rounded-lg shadow-sm cursor-pointer text-center flex flex-col justify-center items-center h-24 hover:border-gray-300 hover:bg-gray-50` )} style={{ opacity: withOpacity ? '0.5' : '1', transform: isDragging ? 'scale(1.1)' : 'scale(1)', ...style, }} {...props}> <span className=' text-3xl'>{props.item.emoji}</span> <div className='flex flex-wrap justify-center items-center gap-x-2 flex-wrap'> <span className='text-lg'>{props.item.id}</span> <span className='text-xs'>{props.item.content}</span> </div> </div>; }); // ๐๏ธ set display name (Fixes TypeScript issue) Item.displayName = 'Item'; export default Item;
I found it much easier to get DndKit working "out of the box" and it required way less hacking around in NextJS and TypeScript. Plus the fact that it works with grid layouts nicely is a huge plus for me.
Although Beautiful DnD is a great module, it's just too much of a pain to get working with NextJS, and the fact that it doesn't work with React Strict Mode is a big no-no for me.
DnD Kit on the other hand is a breeze to get working, it works with React Strict Mode, and it works with grid layouts. It's a clear winner for me.
Now one final thing to note here, is that in my demos I am using only one single column, but both libraries actually allow multiple columns and let you drag and drop items between them, I left this out of my demo page to keep them simple, but it's worth noting that both libraries support this.
I will be adding a new demo page soon to show this in action (most likely using Dnd Kit!!)