You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用@dnd-kit实现多可排序容器?

关于@dnd-kit多可排序容器的实现疑问

我正在几个React项目中学习使用@dnd-kit,这个工具包功能强大、灵活且稳定,但文档有时不够清晰。另外他们有很多不错的示例,但示例没有附带代码说明具体实现方式。

我特别想复现多可排序容器的示例,想请教它的实际工作原理。

我已经尝试了一段时间,做出了一个接近的版本,但仍存在一些问题。以下是我目前的代码:

App.js代码:

import './App.css';
import { useState } from 'react'
import {closestCenter, DndContext} from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy,} from '@dnd-kit/sortable';
import Lane from './Components/Lane';

function App() {
  const [todoItems, setTodoItems] = useState([
    "Item 1",
    "Item 2",
    "Item 3",
    "Item 4",
    "Item 5"
  ]);

  const [doneItems, setDoneItems] = useState([
    "Item 6",
    "Item 7",
    "Item 8",
    "Item 9",
    "Item 10"
  ]);

  return (
    <DndContext onDragOver={handleDragOver} onDragEnd={handleDragEnd} collisionDetection={closestCenter}>
      <div className="App" style={{display: 'flex', justifyContent: 'center', width: '100vw', height: '100vh', backgroundColor:'slateblue', padding: '2rem', boxSizing: 'border-box'}}>
        <SortableContext items={todoItems} strategy={verticalListSortingStrategy}>
          <Lane title="ToDo" items={todoItems} />
        </SortableContext>
        <SortableContext items={doneItems} strategy={verticalListSortingStrategy}>
          <Lane title="Done" items={doneItems} />
        </SortableContext>
      </div>
    </DndContext>
  );

  function handleDragOver (e) {
    const {active, over} = e;

    if(over.id === 'Done' && !doneItems.includes(active.id)) {
      setDoneItems([...doneItems, active.id])
      setTodoItems(todoItems.filter(item => item !== active.id))
    }

    if(over.id === 'ToDo' && !todoItems.includes(active.id)) {
      setTodoItems([...todoItems, active.id])
      setDoneItems(doneItems.filter(item => item !== active.id))
    }
  }
  
  function handleDragEnd (e) {
    const {active, over} = e;
    const container = over.id;
    const title = active.data.current?.title ?? "";
    const index = active.data.current?.index ?? 0;
    const parent = active.data.current?.parent ?? "ToDo";
  }
}

export default App;

Lane.js代码:

import {useDroppable} from '@dnd-kit/core';
import Item from './Item';

const Lane = ({ title, items }) => {
    const {isOver, setNodeRef} = useDroppable({
        id: title,
      });
    const style = {
        backgroundColor: 'slategray',
        opacity: isOver ? '0.8' : '1',
        width: '300px', height: '500px', margin: '2rem'
    };

    return (
        <div ref={setNodeRef} style={style}>
            <h3>{title}</h3>
            {items.map((item, index) => {
                return <Item key={index} title={item} index={index} parent={title} />
            })}
        </div>
    )
}

export default Lane

Item.js代码:

import React from 'react';
import {useSortable} from '@dnd-kit/sortable';
import { CSS } from "@dnd-kit/utilities";

const Item = ({title, index, parent}) => {
    const {attributes, listeners, setNodeRef, transform} = useSortable({
        id: title,
        data: {
            title,
            index,
            parent,
        }
      });
      const style = {
        transform: transform ? CSS.Translate.toString(transform) : undefined,
        borderTop: '1px solid blue',
        padding: '1rem',
        boxShadow: '0px 0px 5px 2px #2121213b'
      }
    
      
    return (
        <div ref={setNodeRef} style={style} {...listeners} {...attributes}>
          {title}
        </div>
  )
}

export default Item

文档中提到:

在这个示例中,我们会使用DndContext的onDragOver回调来检测可拖拽元素何时移动到不同容器上,以便在拖拽过程中将其插入到新容器中。

所以我在dragOver函数中做了类似处理,但我的dragEnd函数还没完成——我先尝试让第一部分正常工作。

求帮助!


问题分析与解决方案

核心问题点

  1. onDragOver频繁触发状态更新:该回调会在拖拽过程中持续触发,当前逻辑会导致状态被重复修改,引发不必要的重渲染,甚至出现元素异常。
  2. 容器内排序逻辑缺失:仅实现了跨容器转移,无法在同一容器内拖拽调整元素顺序。
  3. useDroppable与SortableContext冲突:Lane组件的可拖拽目标会干扰Item的碰撞检测,导致拖拽时目标判断混乱。

正确实现思路

多可排序容器的核心是:

  • 用全局SortableContext管理所有可排序项,统一识别元素位置关系
  • 仅在onDragEnd中处理状态更新(仅触发一次),区分容器内排序和跨容器转移两种场景
  • 移除冗余的useDroppable,依赖SortableContext完成碰撞检测

修正后的代码

App.js

import './App.css';
import { useState, useCallback } from 'react'
import { closestCenter, DndContext } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
import Lane from './Components/Lane';

function App() {
  const [todoItems, setTodoItems] = useState([
    "Item 1", "Item 2", "Item 3", "Item 4", "Item 5"
  ]);
  const [doneItems, setDoneItems] = useState([
    "Item 6", "Item 7", "Item 8", "Item 9", "Item 10"
  ]);

  // 聚合所有可排序项ID,供全局SortableContext使用
  const getAllItemIds = useCallback(() => [
    ...todoItems,
    ...doneItems
  ], [todoItems, doneItems]);

  const handleDragEnd = useCallback((event) => {
    const { active, over } = event;

    if (!over || active.id === over.id) return;

    // 读取源容器和目标容器信息
    const sourceContainer = active.data.current?.container;
    const targetContainer = over.data.current?.container;

    // 同一容器内排序
    if (sourceContainer === targetContainer) {
      const sourceList = sourceContainer === 'ToDo' ? todoItems : doneItems;
      const setList = sourceContainer === 'ToDo' ? setTodoItems : setDoneItems;

      const oldIndex = sourceList.indexOf(active.id);
      const newIndex = sourceList.indexOf(over.id);

      if (oldIndex !== -1 && newIndex !== -1) {
        setList(arrayMove(sourceList, oldIndex, newIndex));
      }
    } 
    // 跨容器转移
    else {
      // 从源容器移除元素
      const sourceList = sourceContainer === 'ToDo' ? todoItems : doneItems;
      const setSourceList = sourceContainer === 'ToDo' ? setTodoItems : setDoneItems;
      setSourceList(sourceList.filter(item => item !== active.id));

      // 插入到目标容器的对应位置
      const targetList = targetContainer === 'ToDo' ? todoItems : doneItems;
      const setTargetList = targetContainer === 'ToDo' ? setTodoItems : setDoneItems;
      const insertIndex = targetList.indexOf(over.id);

      if (insertIndex !== -1) {
        const newTargetList = [...targetList];
        newTargetList.splice(insertIndex, 0, active.id);
        setTargetList(newTargetList);
      } else {
        // 拖拽到容器空白区域时,添加到末尾
        setTargetList([...targetList, active.id]);
      }
    }
  }, [todoItems, doneItems]);

  return (
    <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
      <div className="App" style={{
        display: 'flex', justifyContent: 'center', width: '100vw',
        height: '100vh', backgroundColor:'slateblue', padding: '2rem', boxSizing: 'border-box'
      }}>
        <SortableContext
          items={getAllItemIds()}
          strategy={verticalListSortingStrategy}
        >
          <Lane title="ToDo" items={todoItems} />
          <Lane title="Done" items={doneItems} />
        </SortableContext>
      </div>
    </DndContext>
  );
}

export default App;

Lane.js

import Item from './Item';

const Lane = ({ title, items }) => {
  const style = {
    backgroundColor: 'slategray',
    width: '300px', height: '500px', margin: '2rem',
    padding: '0.5rem', boxSizing: 'border-box'
  };

  return (
    <div style={style}>
      <h3>{title}</h3>
      {items.map((item) => (
        <Item key={item} id={item} container={title} />
      ))}
    </div>
  )
}

export default Lane

Item.js

import React from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from "@dnd-kit/utilities";

const Item = ({ id, container }) => {
  const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
    id,
    data: {
      container
    }
  });

  const style = {
    transform: transform ? CSS.Translate.toString(transform) : undefined,
    transition,
    margin: '0.5rem 0',
    padding: '1rem',
    backgroundColor: 'white',
    boxShadow: '0px 0px 5px 2px #2121213b',
    cursor: 'grab'
  };

  return (
    <div ref={setNodeRef} style={style} {...listeners} {...attributes}>
      {id}
    </div>
  )
}

export default Item

关键改动说明

  1. 全局SortableContext:统一管理所有可排序元素,让@dnd-kit能识别完整的元素位置关系,支持容器内排序和跨容器拖拽。
  2. onDragEnd统一处理逻辑:把所有状态更新放在拖拽结束时执行,避免频繁触发状态修改的问题,同时清晰区分两种拖拽场景。
  3. 移除useDroppable:依赖SortableContext的碰撞检测能力,拖拽到容器空白区域时自动将元素添加到容器末尾。
  4. 使用arrayMove工具函数:利用@dnd-kit提供的工具简化数组内元素的位置交换逻辑,减少代码冗余。

内容的提问来源于stack exchange,提问作者DJFancyAl

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 21:47:50