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

嵌套dndkit SortableContext引发无限重渲染及数据异常求助

问题分析与解决方案

问题概述

开发类Trello任务列表应用时,基于dndkit实现了任务跨列表拖拽排序,新增列表拖拽排序功能后,嵌套使用DndContext与SortableContext导致应用偶尔触发无限重渲染循环,抛出警告:

Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

同时数据中会混入undefined任务,导致后续映射渲染失败。

核心原因

  1. 嵌套DndContext冲突:dndkit的DndContext是全局事件处理器,嵌套多个实例会导致事件冒泡、状态同步混乱,触发不必要的状态更新,进而形成重渲染循环。
  2. 不稳定的依赖项:处理拖拽的useEffect或回调函数依赖了每次渲染都会生成新引用的值(比如未缓存的数组、内联函数),导致钩子反复执行,触发setState形成循环。
  3. 状态更新逻辑漏洞:拖拽结束时的任务/列表状态更新逻辑缺少边界检查,比如错误的数组切片、未验证任务存在性,导致undefined被插入数据数组。

解决方案

1. 移除嵌套的DndContext

使用单个DndContext包裹所有可拖拽元素,通过拖拽类型区分列表和任务:

  • 给列表拖拽元素设置type="list",任务拖拽元素设置type="task"
  • 在handleDragEnd中根据active.data.current.type判断是列表排序还是任务拖拽,分别处理状态更新:
const handleDragEnd = useCallback((event) => {
  const { active, over } = event;
  if (!over) return;

  // 处理列表排序
  if (active.data.current.type === 'list' && over.data.current.type === 'list') {
    setLists(prev => {
      const newLists = [...prev];
      // 执行列表重排序操作
      return newLists;
    });
  }

  // 处理任务跨列表拖拽或同列表排序
  if (active.data.current.type === 'task' && over.data.current.type === 'task') {
    setLists(prev => {
      const newLists = [...prev];
      // 确保任务存在再执行操作
      const sourceTask = newLists[sourceListIndex].tasks.find(t => t.id === active.id);
      if (!sourceTask) return prev;
      // 执行任务移动操作
      return newLists;
    });
  }
}, []);

2. 稳定依赖项与避免不必要重渲染

  • 用useCallback缓存拖拽处理函数,避免每次渲染生成新引用:
const handleDragEnd = useCallback((event) => {
  // 逻辑...
}, []); // 仅依赖必要的稳定值
  • 用useMemo缓存SortableContext的items数组,确保引用稳定:
const listSortableItems = useMemo(() => lists.map(list => list.id), [lists]);
// 在列表的SortableContext中使用
<SortableContext items={listSortableItems} strategy={horizontalListSortingStrategy}>
  {/* 列表元素 */}
</SortableContext>
  • 状态更新优先使用函数式更新,避免依赖当前状态值:
// 错误示例:依赖当前lists,可能导致 stale closure
setLists([...lists, newList]);

// 正确示例:函数式更新,获取最新状态
setLists(prev => [...prev, newList]);

3. 修复undefined任务问题

在任务拖拽的状态更新逻辑中添加边界检查:

const handleTaskDrag = (sourceListId, targetListId, taskId) => {
  setLists(prev => {
    // 找到源列表和目标列表
    const sourceList = prev.find(l => l.id === sourceListId);
    const targetList = prev.find(l => l.id === targetListId);
    if (!sourceList || !targetList) return prev;

    // 找到要移动的任务,确保存在
    const taskToMove = sourceList.tasks.find(t => t.id === taskId);
    if (!taskToMove) return prev;

    // 生成新的列表数组
    return prev.map(list => {
      if (list.id === sourceListId) {
        return { ...list, tasks: list.tasks.filter(t => t.id !== taskId) };
      }
      if (list.id === targetListId) {
        return { ...list, tasks: [...list.tasks, taskToMove] };
      }
      return list;
    });
  });
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 17:28:10