react-dnd拖拽库的使用

1,025 阅读3分钟

需求点

react-dnd 是一个用于实现拖放功能的库,它提供了一套强大而灵活的 API,使得在 React 中实现拖拽交互变得简单。本文尝试实现一个拖拽的demo,实现快速上手react-dnd的目的

基本使用

  1. 实现效果 image.png

image.png

基于react-dnd实现从左边的item,拖拽到右边的container区域,在组件结构上,分为draw组件与drop组件,分别表示两个不同的区域, 其组件结构如下所示

image.png

因为组件之间存在drawList与dropList的数据交互,所以在这个demo里面使用了context共享数据

  1. 设置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>
);
}
  1. 拖拽元素组件

在拖拽组件中,使用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>
    );
};

  1. 放置目标组件

在目标组件中使用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>
    );
};

总结

  1. 在这个demo中使用了context作为共享数据源,并且自定义Provider组件与context hook
  2. 使用了react-dnd实现了基本的拖拽功能,学习了useDrag与useDrop两个hook, 了解了react-dnd的基本使用