需求点
react-dnd 是一个用于实现拖放功能的库,它提供了一套强大而灵活的 API,使得在 React 中实现拖拽交互变得简单。本文尝试实现一个拖拽的demo,实现快速上手react-dnd的目的
基本使用
- 实现效果
基于react-dnd实现从左边的item,拖拽到右边的container区域,在组件结构上,分为draw组件与drop组件,分别表示两个不同的区域, 其组件结构如下所示
因为组件之间存在drawList与dropList的数据交互,所以在这个demo里面使用了context共享数据
- 设置context与provider
// 拖拽的item的类型
export type itemType = { id: number; text: string };
// context的类型
export type contextType = {
drawList: itemType[];
setDrawList: React.Dispatch<React.SetStateAction<itemType[]>>;
dropList: itemType[];
setDropList: React.Dispatch<React.SetStateAction<itemType[]>>;
};
const dndConetxt = createContext<contextType | undefined>(undefined);
// 设置Provider组件
const DndContextProvider = ({ children }: PropsWithChildren) => {
const [drawList, setDrawList] = useState<itemType[]>(() => [
{
id: 1,
text: "item1",
},
{
id: 2,
text: "item2",
},
{
id: 3,
text: "item3",
},
]);
const [dropList, setDropList] = useState<{ id: number; text: string }[]>([]);
return (
<dndConetxt.Provider
value={{
drawList,
setDrawList,
dropList,
setDropList,
}}
>
{children}
</dndConetxt.Provider>
);
};
// 自定义context hook,方便在组件中拿到provider的数据
export const useDndDataConetxt = () => {
const context = useContext(dndConetxt);
if (!context) {
throw new Error(
"useDndDataConetxt must be used within a DndContextProvider"
);
}
return context;
};
最后,在App组件中,将Provider提供到全局
function App() {
return (
<DndContextProvider>
// DndProvider是react-dnd需要设定的provider
<DndProvider backend={HTML5Backend}>
<div className="flex space-x-3 bg-red-100 px-6 py-12">
// draw组件
<Draw />
// drop组件
<Drop />
</div>
</DndProvider>
</DndContextProvider>
);
}
- 拖拽元素组件
在拖拽组件中,使用useDrag这个hook,按照文档中的参数传递,其中 type是拖拽的类型,可以一个也可以多个,多个使用数组存储,这个类型要与目标接受的类型一致才能放置到目标类型中; item 是拖拽要传递的数据,在放置组件中可以拿到这个数据;collect是一个函数,可以拿到拖拽过程中的很多状态,这里我拿一个isDragging判断是否拖拽,来设定样式
const Draw = () => {
// context数据
const { drawList } = useDndDataConetxt();
return (
<div className="flex flex-col gap-2 border-2 border-fuchsia-500 rounded-md h-[300px] w-[300px] px-2 py-1">
{drawList.map((item) => {
return <DrawItem key={item.id} item={item} />;
})}
</div>
);
};
// 拖拽item
const DrawItem = ({ item }: { item: itemType }) => {
const ref = useRef<HTMLDivElement>(null);
const [{ isDragging }, draw] = useDrag({
type: "item", //拖拽的类型
item: item, // draw传递的数据
collect: (monitor) => ({
// 拿到拖拽状态
isDragging: monitor.isDragging(),
}),
});
useEffect(() => {
if (ref.current) {
// 将拖拽的元素传递给draw
draw(ref);
}
}, []);
return (
<div
ref={ref}
className={
"cursor-pointer w-full h-[50px] bg-emerald-500 flex items-center justify-center rounded-md" +
(isDragging ? " border-dashed border-2" : "")
}
>
{item.text}
</div>
);
};
- 放置目标组件
在目标组件中使用useDrop hook可以接受传递过过来的数据,根据数据改变视图;accept要与draw组件中的item的类型一致才能接受
const Drop = () => {
const ref = useRef<HTMLDivElement>(null);
const { dropList, setDropList, setDrawList } = useDndDataConetxt();
const [, drop] = useDrop(() => {
return {
accept: "item",
drop: (item: itemType) => {
// item为drop传递过来的数据
// 改变drawList与dropList的数据,从而改变视图
setDropList((prev) => [...prev, item]);
setDrawList((prev) => {
return prev.filter((prevItem) => prevItem.id !== item.id);
});
},
};
});
useEffect(() => {
if (ref.current) {
drop(ref);
}
}, []);
return (
<div
ref={ref}
className="flex flex-col gap-2 border-2 w-[300px] h-[300px] border-amber-500 rounded-md px-2 py-1"
>
{dropList.map((item) => {
return <DropItem key={item.id} item={item} />;
})}
</div>
);
};
export default Drop;
const DropItem = ({ item }: { item: itemType }) => {
return (
<div className="w-full h-[50px] bg-emerald-500 flex items-center justify-center rounded-md">
{item.text}
</div>
);
};
总结
- 在这个demo中使用了context作为共享数据源,并且自定义Provider组件与context hook
- 使用了react-dnd实现了基本的拖拽功能,学习了useDrag与useDrop两个hook, 了解了react-dnd的基本使用