🌏

[Next.js/React]dnd-kitのsortableの階層構造のコンポーネントでの簡単な実装

2024/06/02に公開

dnd-kitが便利だったのでまとめました。

次のサンプルように、階層ごとにSortableContextで並び替えられるようにしたいとします。

dnd-kitの公式ドキュメントによると、DndContextはnestできるとのこと。
DndContext | @dnd-kit – Documentation https://docs.dndkit.com/api-documentation/context-provider

以下のような実装が可能です(上記サイト引用)

import React from 'react';
import {DndContext} from '@dnd-kit/core';

function App() {
  return (
    <DndContext>
      {/* Components that use `useDraggable`, `useDroppable` */}
      <DndContext>
        {/* ... */}
        <DndContext>
          {/* ... */}
        </DndContext>
      </DndContext>
    </DndContext>
  );
}

以下のコードのように再帰構造で作ります。

"use client";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { DndContext } from "@dnd-kit/core";
import { SortableContext } from "@dnd-kit/sortable";
import { FC } from "react";
import { useState } from "react";

type Props = {
  name: string;
  children: Props[];
}

const DndTest = () => {
  const [items, setItems] = useState<Props>({
    name: 'Root', children: [{
      name: 'A', children: [
        {name: '1', children: []},
        {name: '2', children: []}
      ]
    },{
      name: 'B', children: [
        {name: '1', children: []},
        {name: '2', children: [
          {name: 'a', children: []},
          {name: 'b', children: []}
        ]}
      ]
    },{
      name: 'C', children: [
        {name: '1', children: []},
        {name: '2', children: [
          {name: 'a', children: []},
          {name: 'b', children: []},
          {name: 'c', children: []}
        ]},
        {name: '3', children: []}
      ]
    }]
  });

  const SortableItem = ({id, item}: {id: string; item: Props;}) => {
    const {
      setNodeRef, attributes, listeners, transform, transition, isDragging,
    } = useSortable({ 
      id,
      animateLayoutChanges: () => false
    });

    return (
      <div
        ref={setNodeRef}
        {...attributes}
        {...listeners}
        style={{
        transform: CSS.Translate.toString(transform), // Translateに変更
          transition,
        }}
      >
        <div className="my-2 px-5 border border-gray-300 rounded-md bg-white">
          <p>{item.name}</p>
          {buildView(item, id)}
        </div>
      </div>
    );
  };


  const buildView = (_item: Props, id: string) => {
    const handleDragEnd = (event: any) => {
      const { active, over } = event;
      if (!over) return;
      if (active.id !== over.id) {
        const oldIndex = Number(active.id.toString().split('.').slice(-1))
        const newIndex = Number(over.id.toString().split('.').slice(-1))
        const newArray = [..._item.children]
        newArray.splice(newIndex, 0, newArray.splice(oldIndex, 1)[0])
        _item.children = newArray
        setItems({...items})
      }
    }
  
    return (
      <DndContext onDragEnd={handleDragEnd}>
        <SortableContext items={_item.children.map((item, index) => `${id}.${index}`)}>
          {_item.children.map((item, index) => (
            <SortableItem key={index} id={`${id}.${index}`} item={item}/>
          ))}
        </SortableContext>
      </DndContext>
    )
  }

  return (
    <div className="flex justify-center m-3">
      <div className="w-72 m-2 px-5 border border-gray-300 rounded-md bg-cyan-500">
        {buildView(items, 'id')}
      </div>
    </div>
  );
}

export default DndTest;

このとき、次の点に注意

  • onDragEndで順番を入れ替えた時、domを再描画する必要があるが、useStateは最上位のコンポーネントでないと使えないので、最上位のstateを更新する必要がある。
  • SortableContextのitemsプロパティと、useSortableのidは完全一致させる。

実装面

Discussion