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

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>
  );
}

其他排查点

  1. 确认react-beautiful-dnd与React版本兼容,避免版本不匹配导致的内部逻辑错误;
  2. 检查是否有条件渲染导致Droppable组件被意外卸载(当前代码不存在此问题,但后续若添加空列表隐藏逻辑,需确保Droppable始终存在);
  3. 确保Draggable的draggableId和组件key使用唯一且稳定的ID(当前代码用project.id是正确的)。

内容的提问来源于stack exchange,提问作者Bevan Langley

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 09:55:54