React & NextJS Sortable Lists: Beautiful DnD vs DnD Kit
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.

Spoiler Alert
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
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:
- It doesn't work with NextJS out of the box. You have to do a lot extra work to get it to work with NextJS.
- Although it has Horizontal support, as soon as you want the items to wrap to the next line, it breaks. This is a known issue, and there is a workaround, but it's not ideal.
- To get it working locally you have to disable React Strict mode. This is a big no-no for me, and I'm sure many others.
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
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.
Conclusion
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!!)