Published on

Reactでdnd-kitを使って画像を並び替える方法

Authors

この記事では dnd-kit を使って React & TypeScript で画像の並び替えを実装します。

React で画像の並び替えに使えるライブラリはいくつかあります。

一番 DL 数が多い react-draggable はあまり更新されておらず利用者が減ってきており、次に人気の react-beautiful-dnd は更新が停止されています。そこで最近勢いがあり TypeScript に対応している dnd-kit を採用することにしました。

NPMダウンロードのトレンド

各ライブラリの NPM ダウンロード数トレンド

今回実装したものはこちら

結果は以下の Codesandbox を見ていただければと思います。

実装の流れ

まず DndContext、SortableContext で囲む

ドラッグ&ドロップをしたい要素を DndContext と SortableContext で囲みます。

export default function App() {
  return (
    <DndContext
      sensors={sensors}
      collisionDetection={closestCenter}
      onDragStart={handleDragStart}
      onDragEnd={handleDragEnd}
      onDragCancel={handleDragCancel}
    >
      <SortableContext items={items} strategy={rectSortingStrategy}>
        ...
      </SortableContext>
    </DndContext>
  )
}

DndContext はドラッグ&ドロップにおける各コンポーネント同士の状態管理用で、SortableContext は各コンポーネントで使われる useSortable のために使われます。

DndContext の sensors ではどのような入力に対してドラッグ&ドロップを有効にするか設定します。 少しややこしいですが下記のように実装します。

import {
  useSensors,
  PointerSensor,
  TouchSensor,
} from "@dnd-kit/core"

...

const sensors = useSensors(useSensor(PointerSensor), useSensor(TouchSensor))

PointerSensor、TouchSensor は、パソコンのマウスカーソルとスマホなどのタッチスクリーンに対してのものとなります。 その他、キーボードのキー入力に対するものなどがあります。

DndContext の collisionDetection については要素の衝突判定アルゴリズムを設定します。今回は closestCenter というアルゴリズムを選択しています。

衝突判定アルゴリズムについて詳しい解説が公式サイトにありますので興味がある方はそちらをご覧ください。

onDragStart、onDragEnd などはドラッグ時に発火するコールバックを設定します。今回は画像の並び替えをしたいのでドラッグ完了時に画像配列のステートを更新します。

操作するコンポーネントを作成

ドラッグする要素として、今回の実装では SortableItem というコンポーネントを作成しています。その中で useSortable hook を呼び出すことでドラッグ中なのか等を検出できます。

type Props = {
  item: TItem
} & HTMLAttributes<HTMLDivElement>

const SortableItem = ({ item, ...props }: Props) => {
  const { attributes, isDragging, listeners, setNodeRef, transform, transition } = useSortable({
    id: item.id,
  })

  const styles = {
    transform: CSS.Transform.toString(transform),
    transition: transition || undefined,
  }

  return (
    <Item
      item={item}
      ref={setNodeRef}
      style={styles}
      isOpacityEnabled={isDragging}
      {...props}
      {...attributes}
      {...listeners}
    />
  )
}

export default SortableItem

ちなみに Item コンポーネントに分けているのは、この後書いている DragOverlay でドラッグされた要素を元の場所にも表示するためです。 DragOverlay を使わないのであれば Item コンポーネントに分けなくても良いでしょう。

type Props = {
  item: TItem
  isOpacityEnabled?: boolean
  isDragging?: boolean
} & HTMLAttributes<HTMLDivElement>

const Item = forwardRef<HTMLDivElement, Props>(
  ({ item, isOpacityEnabled, isDragging, style, ...props }, ref) => {
    const styles: CSSProperties = {
      opacity: isOpacityEnabled ? '0.4' : '1',
      cursor: isDragging ? 'grabbing' : 'grab',
      lineHeight: '0.5',
      transform: isDragging ? 'scale(1.05)' : 'scale(1)',
      ...style,
    }

    return (
      <div ref={ref} style={styles} {...props}>
        <img
          src={item.imageUrl}
          alt={`${item.id}`}
          style={{
            borderRadius: '8px',
            boxShadow: isDragging
              ? 'none'
              : 'rgb(63 63 68 / 5%) 0px 0px 0px 1px, rgb(34 33 81 / 15%) 0px 1px 3px 0px',
            maxWidth: '100%',
            objectFit: 'cover',
          }}
        />
      </div>
    )
  }
)

export default Item

DragOverlay について

DragOverlay コンポーネントはドラッグ時に要素を重複して表示したい場合などに有効です。 今回はドラッグ時にドラッグされた要素の半透明画像を元の場所に表示したかったので使いました。

<DragOverlay adjustScale style={{ transformOrigin: '0 0 ' }}>
  {activeItem ? <Item item={activeItem} isDragging /> : null}
</DragOverlay>

activeItem はドラッグされた要素です。

DragOverlayのイメージ

まとめ

dnd-kit は高機能でありながら要点を押さえればかなり簡単にドラッグ&ドロップの UI を実装できました。 TypeScript にも対応しているので今後利用者は増えていくと思います。

参考にされる方はCodesandbox のコードをご確認いただければ思います