使用DndProvider后导入useDrop仍触发drag drop context错误
问题:使用useDrop Hook后抛出'Uncaught Invariant Violation: Expected drag drop context'错误
主文件已用DndProvider包裹组件,此前运行正常,但导入并使用useDrop Hook后出现上述错误。相关代码如下:
/* eslint-disable no-unused-vars */ import React, { useState } from 'react' import Sidebar from './AAComponents/AASidebar' import DraggableItem from './AAComponents/AADraggableItem' import DndProviderWrapper from './AADndProviderWrapper' import { DndProvider, useDrop } from 'react-dnd' import { HTML5Backend } from 'react-dnd-html5-backend' const AAWhatsapp = () => { const [draggedComponents, setDraggedComponents] = useState([]) const [canvasSize, setCanvasSize] = useState({ width: 800, height: 600 }) const handleSidebarItemClick = (componentType) => { setDraggedComponents((prev) => [...prev, { type: componentType, position: { x: 0, y: 0 } }]) } const handleDrag = (index, delta) => { setDraggedComponents((prev) => { const updatedComponents = [...prev] updatedComponents[index].position.x += delta.x updatedComponents[index].position.y += delta.y const canvasWidth = canvasSize.width const canvasHeight = canvasSize.height if (updatedComponents[index].position.x < 0) { updatedComponents[index].position.x = 0 } else if (updatedComponents[index].position.x + 100 > canvasWidth) { updatedComponents[index].position.x = canvasWidth - 100 } if (updatedComponents[index].position.y < 0) { updatedComponents[index].position.y = 0 } else if (updatedComponents[index].position.y + 50 > canvasHeight) { updatedComponents[index].position.y = canvasHeight - 50 } return updatedComponents }) } const handleDropFromSidebar = (item) => { const newItem = { type: item.type, position: { x: 100, y: 100 } } setDraggedComponents((prev) => [...prev, newItem]) } const [, drop] = useDrop({ accept: 'SIDEBAR_ITEM', drop: (item) => handleDropFromSidebar(item) }) return ( <DndProvider backend={HTML5Backend}> <div style={{ display: 'flex' }}> <Sidebar onItemClick={handleSidebarItemClick} /> <div ref={drop} style={{ flex: 1, padding: '20px', position: 'relative', width: canvasSize.width, height: canvasSize.height, border: '1px solid #ccc', overflow: 'hidden' }} > {draggedComponents.map((component, index) => ( <DraggableItem key={index} index={index} component={component} onDrag={handleDrag} /> ))} </div> </div> </DndProvider> ) } export default AAWhatsapp
其余关联代码:
AASidebar.js
/* eslint-disable no-unused-vars */ import React from 'react' import SidebarItem from './AASidebarItem' const AASidebar = ({ onItemClick }) => { const handleItemClick = (component) => { onItemClick(component) } return ( <div className='side' style={{ width: '200px', background: '#f0f0f0', padding: '20px' }}> <SidebarItem type='Text' /> <SidebarItem type='Image' /> </div> ) } export default AASidebar
AASidebarItem.js
import React from 'react' import { useDrag } from 'react-dnd' const AASidebarItem = ({ type }) => { const [{ isDragging }, drag] = useDrag({ type: 'SIDEBAR_ITEM', // Set a unique type for sidebar items item: { type }, collect: (monitor) => ({ isDragging: !!monitor.isDragging() }) }) return ( <div ref={drag} className='m-1 bg-success cursor-pointer' style={{ opacity: isDragging ? 0.5 : 1 }} > {type} </div> ) } export default AASidebarItem
AADraggableItem.js
import React from 'react' import { useDrag } from 'react-dnd' const AADraggableItem = ({ component, index, onDrag }) => { const [{ isDragging }, drag] = useDrag({ type: 'COMPONENT', item: { type: component.type, index }, collect: (monitor) => ({ isDragging: !!monitor.isDragging() }), end: (item, monitor) => { const delta = monitor.getDifferenceFromInitialOffset() if (delta) { onDrag(index, delta) } } }) const getDynamicStyles = () => { switch (component.type) { case 'Text': return { width: '100px', height: '50px', fontSize: '16px', fontWeight: 'bold' } case 'Image': return { width: '150px', height: '100px' } default: return { width: '100px', height: '50px' } } } const dynamicStyles = getDynamicStyles() const renderItemContent = (component) => { switch (component.type) { case 'Text': return <span>A Text Item</span> case 'Image': return <img src="path/to/image.png" alt="Image Item" style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> default: return <span>{component.type}</span> } } return ( <div ref={(node) => drag(node)} style={{ width: dynamicStyles.width, height: dynamicStyles.height, background: isDragging ? '#87CEEB' : '#e0e0e0', position: 'absolute', left: component.position.x, top: component.position.y, cursor: 'move', opacity: isDragging ? 0 : 1, border: '1px solid #000', ...dynamicStyles }} > {renderItemContent(component)} </div> ) } export default AADraggableItem
已尝试用独立的DndProviderWrapper包裹整个应用,但问题未解决。
错误原因
你在AAWhatsapp组件内部直接调用了useDrop Hook,但DndProvider是该组件返回值里才渲染的——组件执行useDrop时,DndProvider还未挂载,当前组件并不在DndContext的范围内,所以react-dnd找不到上下文,抛出该错误。
核心问题:Hook调用时机早于DndProvider的挂载,组件本身不在DndContext包裹下。
修复方案
把useDrop的逻辑抽离到独立子组件中,让子组件被DndProvider包裹,这样调用useDrop时就能正常获取上下文。
步骤1:创建DropCanvas组件
新建专门处理画布drop逻辑的组件:
import React from 'react'; import { useDrop } from 'react-dnd'; const DropCanvas = ({ canvasSize, draggedComponents, DraggableItem, onDrag, onAddComponent }) => { const handleDropFromSidebar = (item) => { const newItem = { type: item.type, position: { x: 100, y: 100 } }; onAddComponent(newItem); }; const [, drop] = useDrop({ accept: 'SIDEBAR_ITEM', drop: (item) => handleDropFromSidebar(item) }); return ( <div ref={drop} style={{ flex: 1, padding: '20px', position: 'relative', width: canvasSize.width, height: canvasSize.height, border: '1px solid #ccc', overflow: 'hidden' }} > {draggedComponents.map((component, index) => ( <DraggableItem key={index} index={index} component={component} onDrag={onDrag} /> ))} </div> ); }; export default DropCanvas;
步骤2:修改AAWhatsapp组件
替换原画布部分为DropCanvas,确保DndProvider包裹整个子组件树:
/* eslint-disable no-unused-vars */ import React, { useState } from 'react' import Sidebar from './AAComponents/AASidebar' import DraggableItem from './AAComponents/AADraggableItem' import DropCanvas from './DropCanvas'; // 导入新组件 import { DndProvider } from 'react-dnd' import { HTML5Backend } from 'react-dnd-html5-backend' const AAWhatsapp = () => { const [draggedComponents, setDraggedComponents] = useState([]) const [canvasSize, setCanvasSize] = useState({ width: 800, height: 600 }) const handleSidebarItemClick = (componentType) => { setDraggedComponents((prev) => [...prev, { type: componentType, position: { x: 0, y: 0 } }]) } const handleDrag = (index, delta) => { setDraggedComponents((prev) => { const updatedComponents = [...prev] updatedComponents[index].position.x += delta.x updatedComponents[index].position.y += delta.y const canvasWidth = canvasSize.width const canvasHeight = canvasSize.height if (updatedComponents[index].position.x < 0) { updatedComponents[index].position.x = 0 } else if (updatedComponents[index].position.x + 100 > canvasWidth) { updatedComponents[index].position.x = canvasWidth - 100 } if (updatedComponents[index].position.y < 0) { updatedComponents[index].position.y = 0 } else if (updatedComponents[index].position.y + 50 > canvasHeight) { updatedComponents[index].position.y = canvasHeight - 50 } return updatedComponents }) } const handleAddComponent = (newItem) => { setDraggedComponents((prev) => [...prev, newItem]); }; return ( <DndProvider backend={HTML5Backend}> <div style={{ display: 'flex' }}> <Sidebar onItemClick={handleSidebarItemClick} /> <DropCanvas canvasSize={canvasSize} draggedComponents={draggedComponents} DraggableItem={DraggableItem} onDrag={handleDrag} onAddComponent={handleAddComponent} /> </div> </DndProvider> ) } export default AAWhatsapp
额外注意事项
- 所有使用
useDrag或useDrop的组件,必须直接或间接被DndProvider包裹,且Hook调用必须在DndContext范围内。 - 不要在同一个组件里既渲染DndProvider又调用react-dnd的Hook,因为组件执行Hook时,DndProvider还未完成挂载,上下文不存在。
内容的提问来源于stack exchange,提问作者Vin
相关产品推荐
相关产品推荐

