Building a Drag-and-Drop List with React and dnd-kit/core

Implement a seamless drag-and-drop list in React using dnd-kit/core, with a walkthrough of the core code and styling.

TechPublished 2 min read

image

Drag-and-drop is a common feature in modern web applications, letting users reorder items seamlessly. In this tutorial, we'll explore how to implement drag-and-drop functionality in a React application using the dnd-kit/core library. We'll use this library's core components to build a simple sortable list of draggable items.

Project Setup

Before diving into the implementation, let's install the required dependencies. Install @dnd-kit/core in your React project.

npm i @dnd-kit/core @dnd-kit/sortable

From here, we'll build out the components for our draggable list.

Creating the DraggableList Component

The DraggableList component acts as the container for the draggable items. Here's the code for the DraggableList.tsx file:

DraggableList.tsx(DraggableList)

import {
  DndContext,
  KeyboardSensor,
  PointerSensor,
  UniqueIdentifier,
  closestCenter,
  useSensor,
  useSensors
} from '@dnd-kit/core';
import {
  SortableContext,
  horizontalListSortingStrategy,
  rectSortingStrategy,
  sortableKeyboardCoordinates,
  useSortable,
  verticalListSortingStrategy
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { ComponentProps, FC, ReactNode, useMemo } from 'react';

export interface HasId {
  id: UniqueIdentifier;
}

type Props<T extends HasId> = {
  items: T[];
  onDragStart?: ComponentProps<typeof DndContext>['onDragStart'];
  onDragEnd?: ComponentProps<typeof DndContext>['onDragEnd'];
  onDragOver?: ComponentProps<typeof DndContext>['onDragOver'];
  layout: 'horizontal' | 'vertical' | 'grid';
  children: ReactNode;
};
// ドラッグ&ドロップ可能なリストアイテム
export const DraggableList: FC<Props<HasId>> = ({
  items,
  onDragStart,
  onDragEnd,
  onDragOver,
  layout,
  children
}) => {
  // ドラッグ&ドロップする時に許可する入力
  const sensors = useSensors(
    useSensor(PointerSensor),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates
    })
  );

  // リストの種類
  const strategy = useMemo(() => {
    switch (layout) {
      case 'horizontal':
        return horizontalListSortingStrategy;
      case 'vertical':
        return verticalListSortingStrategy;
      case 'grid':
      default:
        return rectSortingStrategy;
    }
  }, [layout]);

  return (
    <DndContext
      sensors={sensors}
      collisionDetection={closestCenter}
      onDragStart={onDragStart}
      onDragEnd={onDragEnd}
      onDragOver={onDragOver}
    >
      <SortableContext items={items} strategy={strategy}>
        <ul>{children}</ul>
      </SortableContext>
    </DndContext>
  );
};

This component sets up the DndContext and SortableContext provided by @dnd-kit/core. It also defines the DraggableItem component that wraps each item in the list.

Building the Draggable Item

Each draggable item in the list is represented by the DraggableItem component. It also includes a DraggableHandle to improve the user interaction. Here's the code:

DraggableList.tsx(DraggableItem)

// ドラッグ&ドロップ可能なリストアイテム
export const DraggableItem: FC<{
  id: HasId['id'];
  children: ReactNode;
}> = ({ id, children }) => {
  const { setNodeRef, transform, transition } = useSortable({ id });

  const style = {
    transform: CSS.Transform.toString(transform),
    transition
  };

  return (
    <li ref={setNodeRef} style={style}>
      {children}
    </li>
  );
};

// MyDraggableItemをドラッグ&ドロップするためのハンドル
export const DraggableHandle: FC<{
  id: HasId['id'];
  children: ReactNode;
}> = ({ id, children }) => {
  const { attributes, listeners } = useSortable({ id });

  return (
    <div {...attributes} {...listeners}>
      {children}
    </div>
  );
};

This component uses the useSortable hook from @dnd-kit/core to handle the drag-and-drop behavior.

Implementing Drag-and-Drop in the Page Component

In the main page component (page.tsx), we import and use DraggableList to build a draggable list of products. Here's the code:

This uses Next.js, so it's named page.tsx, but it works just as well as a regular React component.

'use client';

import { DragEndEvent, DragStartEvent } from '@dnd-kit/core';
import { faBars } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FC, useCallback, useState } from 'react';
import { DraggableHandle, DraggableItem, DraggableList } from './_components/DraggableList';

// リソース:商品の型
type Product = {
  id: string;
  name: string;
  price: number;
};

const Products = [
  {
    id: '1',
    name: '商品1',
    price: 100
  },
  {
    id: '2',
    name: '商品2',
    price: 200
  },
  {
    id: '3',
    name: '商品3',
    price: 300
  }
];

// 商品を表示するリストアイテム
const ProductItem: FC<{
  product: Product;
}> = ({ product }) => (
  <DraggableItem id={product.id}>
    <div className="m-4 h-[100px] w-[200px] items-center rounded-md border-2 p-2 shadow-sm">
      <div className="flex space-x-4">
        <DraggableHandle id={product.id}>
          <FontAwesomeIcon icon={faBars} className="m-4" size="lg" />
        </DraggableHandle>
        <p>{product.name}</p>
        <p>¥ {product.price}</p>
      </div>
    </div>
  </DraggableItem>
);

// ドラッグ&ドロップ可能な商品リスト
const Page = () => {
  // 一覧表示する商品群
  const [products, setProducts] = useState<Product[]>(Products);

  const onDragStart = useCallback((e: DragStartEvent) => {
    console.log('onDragStart', e);
  }, []);

  const onDragEnd = useCallback((e: DragEndEvent) => {
    console.log('onDragEnd', e);
  }, []);

  return (
    <DraggableList
      items={products}
      onDragStart={onDragStart}
      onDragEnd={onDragEnd}
      layout={'vertical'}
    >
      {products.map((product) => (
        <ProductItem key={product.id} product={product} />
      ))}
    </DraggableList>
  );
};

export default Page;

This component renders a list of products, and the onDragStart and onDragEnd callbacks log the corresponding events to the console.

Styling and Customization

Feel free to customize the styling of the draggable items to match your application's design. The provided code includes basic styling using Tailwind CSS.

Summary

In this tutorial, we implemented a simple draggable list using react and @dnd-kit/core. This library provides a robust toolset for handling drag-and-drop interactions, making it easy to build a seamless user experience.

You can take this implementation further with animations, custom drag handles, and more. Explore the @dnd-kit/core documentation for additional options and possibilities.