React+TypeScript中react-beautiful-dnd拖拽报错:找不到Droppable ID求助
拖拽报错:
Invariant failed: Cannot find droppable entry with id [active] 我用TypeScript和React开发了一个简易项目管理应用,支持用户添加项目(表单包含标题、描述、参与人数3个字段),提交后新项目会进入「Active Projects」列表;同时借助react-beautiful-dnd实现项目拖拽到「Completed Projects」列表的功能,用uuid生成项目唯一ID。
目前添加项目功能正常,但尝试拖拽项目时,点击并按住项目就会抛出错误:Invariant failed: Cannot find droppable entry with id [active]。
应用代码结构
components/ProjectInputForm.tsx
import React, { useState } from 'react'; type ProjectInputFormProps = { onAdd: (title: string, description: string, people: number) => void; }; const ProjectInputForm: React.FC<ProjectInputFormProps> = ({ onAdd }) => { const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [people, setPeople] = useState(0); const submitHandler = (event: React.FormEvent) => { event.preventDefault(); onAdd(title, description, people); }; return ( <form onSubmit={submitHandler}> <input type="text" value={title} onChange={e => setTitle(e.target.value)} placeholder="Title" /> <textarea value={description} onChange={e => setDescription(e.target.value)} placeholder="Description" /> <input type="number" value={people} onChange={e => setPeople(parseInt(e.target.value))} placeholder="People" /> <button type="submit">Add Project</button> </form> ); }; export default ProjectInputForm;
components/ProjectList.tsx
import { Droppable, Draggable } from 'react-beautiful-dnd'; type ProjectType = { id: string; title: string; description: string; people: number; }; type ProjectListProps = { projects: ProjectType[]; listType: 'active' | 'completed'; onDelete: (id: string) => void; }; const ProjectList: React.FC<ProjectListProps> = ({ projects, listType, onDelete, }) => { return ( <Droppable droppableId={listType}> {provided => ( <div {...provided.droppableProps} ref={provided.innerRef}> <h2> {listType.charAt(0).toUpperCase() + listType.slice(1)} Projects </h2> {projects.map((project, index) => ( <Draggable key={project.id} draggableId={project.id} index={index}> {provided => ( <div {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef} > <h3>{project.title}</h3> <p>{project.description}</p> <p>{project.people}</p> <button onClick={() => onDelete(project.id)}>Delete</button> </div> )} </Draggable> ))} {provided.placeholder} </div> )} </Droppable> ); }; export default ProjectList;
App.tsx
import { useState } from 'react'; import { DragDropContext } from 'react-beautiful-dnd'; import { v4 as uuidv4 } from 'uuid'; import ProjectInputForm from './components/ProjectInputForm'; import ProjectList from './components/ProjectList'; type ProjectType = { id: string; title: string; description: string; people: number; }; function App() { const [activeProjects, setActiveProjects] = useState<ProjectType[]>([]); const [completedProjects, setCompletedProjects] = useState<ProjectType[]>([]); const addProject = (title: string, description: string, people: number) => { const newProject: ProjectType = { id: uuidv4(), title, description, people, }; setActiveProjects([...activeProjects, newProject]); }; const deleteProject = (list: 'active' | 'completed', id: string) => { if (list === 'active') { setActiveProjects(activeProjects.filter(project => project.id !== id)); } else { setCompletedProjects( completedProjects.filter(project => project.id !== id) ); } }; const handleDragEnd = (result: any) => { if (!result.destination) return; const { source, destination } = result; if (source.droppableId !== destination.droppableId) { setActiveProjects(prevActiveProjects => { const sourceList = source.droppableId === 'active' ? prevActiveProjects : completedProjects; const destinationList = destination.droppableId === 'active' ? prevActiveProjects : completedProjects; const [removed] = sourceList.splice(source.index, 1); destinationList.splice(destination.index, 0, removed); if (source.droppableId === 'active') { setCompletedProjects([...destinationList]); } else { setCompletedProjects([...sourceList]); } return [...sourceList]; }); } }; return ( <DragDropContext onDragEnd={handleDragEnd}> <ProjectInputForm onAdd={addProject} /> <ProjectList projects={activeProjects} listType="active" onDelete={id => deleteProject('active', id)} /> <ProjectList projects={completedProjects} listType="completed" onDelete={id => deleteProject('completed', id)} /> </DragDropContext> ); } export default App;
解决方向
核心问题:状态更新的闭包陷阱
你的handleDragEnd函数中,更新activeProjects时直接引用了外部的completedProjects状态,这会获取到组件渲染时的旧状态值,导致拖拽过程中状态不同步,进而让react-beautiful-dnd内部无法正确找到对应的droppable节点,抛出找不到ID的错误。
修复方案:使用函数式更新获取最新状态
修改handleDragEnd逻辑,通过函数式更新获取最新的activeProjects和completedProjects状态,避免闭包问题:
const handleDragEnd = (result: any) => { if (!result.destination) return; const { source, destination } = result; // 同列表且同位置拖拽,直接返回 if (source.droppableId === destination.droppableId && source.index === destination.index) { return; } if (source.droppableId !== destination.droppableId) { // 同步更新两个列表状态 setActiveProjects((prevActive) => { setCompletedProjects((prevCompleted) => { let sourceArr: ProjectType[]; let destArr: ProjectType[]; // 确定源列表和目标列表 if (source.droppableId === 'active') { sourceArr = [...prevActive]; destArr = [...prevCompleted]; } else { sourceArr = [...prevCompleted]; destArr = [...prevActive]; } // 移动项目 const [movedProject] = sourceArr.splice(source.index, 1); destArr.splice(destination.index, 0, movedProject); // 返回对应更新后的列表 if (source.droppableId === 'active') { return destArr; } else { return sourceArr; } }); return source.droppableId === 'active' ? sourceArr : destArr; }); } else { // 同列表内拖拽调整顺序 const updatedList = [...(source.droppableId === 'active' ? activeProjects : completedProjects)]; const [movedItem] = updatedList.splice(source.index, 1); updatedList.splice(destination.index, 0, movedItem); source.droppableId === 'active' ? setActiveProjects(updatedList) : setCompletedProjects(updatedList); } };
更优方案:用useReducer管理状态
对于涉及多个关联状态的更新(比如两个列表的同步修改),使用useReducer会比多个useState更清晰,避免异步更新的问题:
// 定义reducer type State = { activeProjects: ProjectType[]; completedProjects: ProjectType[]; }; type Action = | { type: 'ADD_PROJECT'; payload: ProjectType } | { type: 'DELETE_PROJECT'; payload: { list: 'active' | 'completed'; id: string } } | { type: 'MOVE_PROJECT'; payload: { source: any; destination: any } }; const projectReducer = (state: State, action: Action): State => { switch (action.type) { case 'ADD_PROJECT': return { ...state, activeProjects: [...state.activeProjects, action.payload] }; case 'DELETE_PROJECT': if (action.payload.list === 'active') { return { ...state, activeProjects: state.activeProjects.filter(p => p.id !== action.payload.id) }; } else { return { ...state, completedProjects: state.completedProjects.filter(p => p.id !== action.payload.id) }; } case 'MOVE_PROJECT': const { source, destination } = action.payload; if (source.droppableId === destination.droppableId) { // 同列表调整顺序 const list = source.droppableId === 'active' ? state.activeProjects : state.completedProjects; const updatedList = [...list]; const [item] = updatedList.splice(source.index, 1); updatedList.splice(destination.index, 0, item); return source.droppableId === 'active' ? { ...state, activeProjects: updatedList } : { ...state, completedProjects: updatedList }; } else { // 跨列表移动 const sourceList = source.droppableId === 'active' ? state.activeProjects : state.completedProjects; const destList = destination.droppableId === 'active' ? state.activeProjects : state.completedProjects; const newSourceList = [...sourceList]; const newDestList = [...destList]; const [movedItem] = newSourceList.splice(source.index, 1); newDestList.splice(destination.index, 0, movedItem); return source.droppableId === 'active' ? { ...state, activeProjects: newSourceList, completedProjects: newDestList } : { ...state, activeProjects: newDestList, completedProjects: newSourceList }; } default: return state; } }; // 在App组件中使用 function App() { const [state, dispatch] = useReducer(projectReducer, { activeProjects: [], completedProjects: [] }); const addProject = (title: string, description: string, people: number) => { const newProject: ProjectType = { id: uuidv4(), title, description, people, }; dispatch({ type: 'ADD_PROJECT', payload: newProject }); }; const deleteProject = (list: 'active' | 'completed', id: string) => { dispatch({ type: 'DELETE_PROJECT', payload: { list, id } }); }; const handleDragEnd = (result: any) => { if (!result.destination) return; dispatch({ type: 'MOVE_PROJECT', payload: { source: result.source, destination: result.destination } }); }; // 渲染部分不变,替换状态为reducer管理的state return ( <DragDropContext onDragEnd={handleDragEnd}> <ProjectInputForm onAdd={addProject} /> <ProjectList projects={state.activeProjects} listType="active" onDelete={id => deleteProject('active', id)} /> <ProjectList projects={state.completedProjects} listType="completed" onDelete={id => deleteProject('completed', id)} /> </DragDropContext> ); }
其他排查点
- 确认
react-beautiful-dnd与React版本兼容,避免版本不匹配导致的内部逻辑错误; - 检查是否有条件渲染导致
Droppable组件被意外卸载(当前代码不存在此问题,但后续若添加空列表隐藏逻辑,需确保Droppable始终存在); - 确保
Draggable的draggableId和组件key使用唯一且稳定的ID(当前代码用project.id是正确的)。
内容的提问来源于stack exchange,提问作者Bevan Langley
相关产品推荐
相关产品推荐

