React Beautiful DND+TypeScript报错:无法找到ID为1的Draggable
React-beautiful-dnd拖拽警告排查与修复
问题背景
开发基于React+TypeScript的项目,使用react-beautiful-dnd实现API列表项的拖拽排序功能,要求拖拽时同步更新显示顺序与项的number属性。但每次拖拽元素时,控制台出现如下警告:
react_devtools_backend.js:4012 react-beautiful-dndUnable to find draggable with id: 1👷 This is a development-only message. It will be removed in production builds.
核心代码
Categories.tsx(逻辑主文件)
// Global imports import {DragDropContext,Droppable, DropResult} from "react-beautiful-dnd"; // Local imports // Context import { menuContext } from "../menu"; // Components import CategoryItem from "./cateogory"; // Types import Category from "../../types/category"; function Categories(){ const {categories, setCategories} = useContext(menuContext); /*functions and logic*/ const onDragEnd = (result:DropResult) => { const {destination, source} = result; if (!destination) return; const items = Array.from(categories); const [newOrder] = items.splice(source.index, 1); items.splice(destination.index, 0, newOrder); setCategories(items); } return ( <div className="greyBox"> <header className="header_menu"> <p>All your categories</p> <div className="searchContainer"> <AiOutlineSearch className="searchIcon" /> <input type="text" placeholder="Search" className="searchBar" value={search} onChange={(e) => handleModification(e)} /> </div> </header> <DragDropContext onDragEnd={onDragEnd}> <Droppable droppableId="categories"> {(provided) => <div {...provided.droppableProps} ref={provided.innerRef} className="categoriesContainer" > {filteredCategories.map((category: Category, idx: number) => { return ( <CategoryItem key={category.id} idx={idx} category={category}/> ) })} </div>} </Droppable> </DragDropContext> <button onClick={(e) => confirmChanges(e)} className="submitBTN bottomButton">Confirm changes</button> </div> ) } export default Categories;
Category.tsx(列表项组件)
// Global imports import React, { useContext, useState } from "react"; import { Draggable } from "react-beautiful-dnd"; // Local imports // Context import { menuContext } from "../menu"; // Types import Category from "../../types/category"; function CategoryItem(props:{category:Category, idx:number}) { const {categories, setCategories} = useContext(menuContext); const {selectedCategory, setSelectedCategory} = useContext(menuContext); /*Functions and various small things*/ let category = categories.find((category:Category) => category.id === props.category.id); return ( <Draggable key={props.category.id} draggableId={props.category.id.toString()} index={props.idx}> {(provided, snapshot) => ( <div className={selectedCategory === category.id ? "category activeCategory":"category"} onClick={() => handleCategoryClick()} style={{backgroundColor: category.isActive ? "" : "#FF7B7B"}} ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} > <div className="alwaysActiveCategory" style={{alignItems: props.category.id === selectedCategory ? "center" : "normal"}}> <MdOutlineDragIndicator className="dragIcon" /> <p style={{display: props.category.id === selectedCategory ? "none" : "block" }}>{category.name}</p> </div> <aside style={{display: selectedCategory === category.id ? "flex":"none"}}> <form className="modifyCategory" onClick={(e) => e.stopPropagation()} onSubmit={e => updateCategory(e)}> <label htmlFor="categoryName">Category Name</label> <p id="categoryProblem3">The name must be given</p> <input value={name} onChange={(e) => changeName(e)} type="text" name="categoryName" id="categoryName" /> <label htmlFor="categoryDescription">Category Description</label> <p id="categoryProblem4">The description must be given</p> <textarea value={description} onChange={(e) => changeDescription(e)} name="categoryDescription" id="categoryDescription" style={{maxWidth: "85%"}} /> <button>Update Category</button> <div className="manageCategory"> <button onClick={(e) => hideCategory(e) }> <BiHide className="hideIcon" style={{backgroundColor: categories.find((category: Category) => category.id === props.category.id).isActive ? "#FF7B7B" : "#76CB8E"}} /> {category.isActive ? "Hide Category" : "Show Category"} </button> <button onClick={(e) => deleteCategory(e)}> <BsTrash className="deleteIcon" /> Delete Category</button> </div> </form> </aside> </div> )} </Draggable> ) } export default CategoryItem;
问题根源分析
- 索引不匹配:渲染列表使用
filteredCategories的索引作为Draggable的index,但拖拽逻辑基于原始categories数组。当拖拽后更新categories,如果filteredCategories是搜索过滤后的结果,会导致渲染项的索引与拖拽逻辑中的索引错位,触发"找不到draggable"警告。 - 数据引用不一致:
CategoryItem中通过categories.find重新查找数据,而非直接使用传入的props.category,导致组件依赖context数据而非props,易出现数据不同步。 - 未同步
number属性:拖拽仅调整数组顺序,未更新每个项的number值,既不符合需求,也可能导致后续渲染的标识混乱。
修复步骤
1. 同步拖拽顺序与number属性
修改Categories中的onDragEnd函数,调整数组顺序的同时更新每个项的number值:
const onDragEnd = (result: DropResult) => { const { destination, source } = result; if (!destination || destination.index === source.index) return; const items = Array.from(categories); const [movedItem] = items.splice(source.index, 1); items.splice(destination.index, 0, movedItem); // 同步更新每个category的number属性,从1开始递增 const updatedItems = items.map((item, index) => ({ ...item, number: index + 1 })); setCategories(updatedItems); };
2. 统一索引与数据源
确保Draggable的index与渲染列表一致:
- 如果
filteredCategories是过滤后的结果,拖拽逻辑需基于该列表处理,或确保拖拽时过滤逻辑暂停。示例中保持渲染与拖拽使用同一数据源:
// Categories.tsx中渲染部分 {filteredCategories.map((category: Category, idx: number) => { return ( <CategoryItem key={category.id} idx={idx} category={category} /> ); })}
3. 避免重复查找数据
在CategoryItem中直接使用props.category,而非从context重新查找:
// CategoryItem.tsx中 // 移除该行:let category = categories.find((category:Category) => category.id === props.category.id); const category = props.category;
同时将组件内所有依赖category的逻辑改为使用props.category(或赋值后的category变量)。
4. 验证拖拽一致性
拖拽完成后,检查categories数组的顺序与每个项的number值是否同步,确保后续渲染的列表索引与Draggable的index匹配。
额外注意事项
- 确保
category.id是唯一且稳定的标识,draggableId必须为字符串(当前代码已处理为.toString(),符合要求)。 - 若
filteredCategories动态变化(如搜索),拖拽时建议暂停搜索功能,避免列表项突变导致警告。
内容的提问来源于stack exchange,提问作者Matteo Possamai
相关产品推荐
相关产品推荐

