如何在dndkit拖拽时减少/阻止SortableItem重渲染
dnd-kit拖拽列表重渲染优化方案
核心问题分析
你遇到的频繁重渲染问题,本质是useSortable钩子默认会订阅SortableContext的全局状态变化(如拖拽激活项、悬停项),导致所有列表项跟随重渲染。之前用memo无效,是因为没有限制钩子的订阅范围,且传递了不必要的可变props(如index)。
优化方案
以下是可落地的分层优化步骤,兼顾动画效果与性能:
1. 限制useSortable的状态订阅范围
通过useSortable的select配置,只订阅当前列表项相关的状态,避免全局状态变化触发所有组件重渲染:
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, // 仅订阅当前item的状态,减少重渲染触发条件 select: (state) => ({ isDragging: state.active.id === id, transform: state.items[id]?.transform, transition: state.items[id]?.transition, }), });
2. 正确使用React.memo+稳定props
- 移除
index这类可变props,仅传递稳定的id作为唯一标识 - 用
memo包裹拖拽组件,阻止无意义的props变化重渲染 - 用
useMemo缓存样式对象,避免每次渲染创建新引用
3. 拆分组件:分离拖拽逻辑与展示逻辑
将组件拆分为拖拽容器和纯展示内容两部分,让复杂的展示组件仅在必要时重渲染:
// 纯展示组件,仅在isDragging变化时重渲染 const ItemContent = memo(({ id, isDragging }) => { console.log(`rendered content: ${id}`); return ( <div className={`p-3 ${isDragging ? 'opacity-50' : ''}`}> {id} </div> ); }); // 拖拽容器组件,仅处理拖拽逻辑 const SortableItem = memo(({ id }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, select: (state) => ({ isDragging: state.active.id === id, transform: state.items[id]?.transform, transition: state.items[id]?.transition, }), }); const style = useMemo(() => ({ transform: transform ? CSS.Transform.toString(transform) : undefined, transition, }), [transform, transition]); return ( <div className="border border-red-500 bg-yellow-50 mb-2" ref={setNodeRef} style={style} {...attributes} {...listeners} > <ItemContent id={id} isDragging={isDragging} /> </div> ); });
4. 优化父组件逻辑
移除不必要的props传递,确保items数组仅在拖拽结束时更新:
// App组件中map渲染时,仅传递id {items.map((item) => ( <SortableItem key={item} id={item} /> ))}
完整优化代码
import { useState, memo, useMemo } from "react"; import { DndContext, closestCenter } from "@dnd-kit/core"; import { arrayMove, SortableContext, verticalListSortingStrategy, useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; export function App() { const [items, setItems] = useState(["Item1", "Item2", "Item3"]); const handleDragEnd = (event) => { const { active, over } = event; if (active.id !== over.id) { setItems((prevItems) => { const activeIndex = prevItems.indexOf(active.id); const overIndex = prevItems.indexOf(over.id); return arrayMove(prevItems, activeIndex, overIndex); }); } }; return ( <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}> <div className="p-3"> <SortableContext items={items} strategy={verticalListSortingStrategy} > {items.map((item) => ( <SortableItem key={item} id={item} /> ))} </SortableContext> </div> </DndContext> ); } const ItemContent = memo(({ id, isDragging }) => { console.log(`rendered content: ${id}`); return ( <div className={`p-3 ${isDragging ? 'opacity-50' : ''}`}> {id} </div> ); }); const SortableItem = memo(({ id }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, select: (state) => ({ isDragging: state.active.id === id, transform: state.items[id]?.transform, transition: state.items[id]?.transition, }), }); const style = useMemo(() => ({ transform: transform ? CSS.Transform.toString(transform) : undefined, transition, }), [transform, transition]); console.log(`rendered container: ${id}`); return ( <div className="border border-red-500 bg-yellow-50 mb-2" ref={setNodeRef} style={style} {...attributes} {...listeners} > <ItemContent id={id} isDragging={isDragging} /> </div> ); });
是否值得投入精力?
- 如果列表项数量少(<50个)且结构简单:优化收益低,无需投入
- 如果列表项数量多(>100个)或包含复杂子组件:优化后可显著提升拖拽流畅度,且dnd-kit仍在维护、功能更丰富,完全值得投入
内容的提问来源于stack exchange,提问作者S Y
相关产品推荐
相关产品推荐

