嵌套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任务,导致后续映射渲染失败。
核心原因
- 嵌套DndContext冲突:dndkit的
DndContext是全局事件处理器,嵌套多个实例会导致事件冒泡、状态同步混乱,触发不必要的状态更新,进而形成重渲染循环。 - 不稳定的依赖项:处理拖拽的
useEffect或回调函数依赖了每次渲染都会生成新引用的值(比如未缓存的数组、内联函数),导致钩子反复执行,触发setState形成循环。 - 状态更新逻辑漏洞:拖拽结束时的任务/列表状态更新逻辑缺少边界检查,比如错误的数组切片、未验证任务存在性,导致
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
相关产品推荐
相关产品推荐

