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

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;

问题根源分析

  1. 索引不匹配:渲染列表使用filteredCategories的索引作为Draggable的index,但拖拽逻辑基于原始categories数组。当拖拽后更新categories,如果filteredCategories是搜索过滤后的结果,会导致渲染项的索引与拖拽逻辑中的索引错位,触发"找不到draggable"警告。
  2. 数据引用不一致:CategoryItem中通过categories.find重新查找数据,而非直接使用传入的props.category,导致组件依赖context数据而非props,易出现数据不同步。
  3. 未同步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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 20:30:42