我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Refine 集成 React DND 构建看板:useDrag 与 useDrop 完整实战指南

Refine 集成 React DND 构建看板:useDrag 与 useDrop 完整实战指南 Refine 集成 React DND 构建看板useDrag 与 useDrop 完整实战指南【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine导读本文以 Refine 项目为例系统讲解如何利用 React DND 库react-dndreact-dnd-html5-backend为管理后台构建可拖拽的看板Kanban Board。你会完整掌握DndProvider的接入方式、useDrag/useDrop两个核心 Hook 的用法与类型匹配原理、基于 RefineuseList的数据驱动渲染以及性能优化、错误排查和移动端 Touch Backend 支持等进阶技巧。文末附有可直接运行的完整示例工程位于 examples/blog-react-dnd。背景为什么用 React DND 而不是原生拖拽 APIHTML 原生 Drag-and-Drop API 是 Web 平台的一项先驱能力但其使用门槛较高——实现一次拖拽往往需要编写大量样板代码对初学者并不友好。为此社区出现了 React-Beautiful-DND、React DND 等封装库把底层细节收敛起来。React DND 提供的是低层级lower-level的解决方案它不像其他库那样提供现成组件而是通过useDrag和useDrop两个 Hook 包装你的现有组件并向其中注入拖拽行为相关的 props。这意味着你可以完全掌控视觉表现把精力放在界面细节上而拖拽的核心机制由库来负责。它基于 HTML5 拖拽 API 实现同时支持鼠标与触摸事件非常适合在管理后台中构建高度可维护的拖放界面。本文的示例运行在一个 Refine 项目中。Refine 是一个 headless 的 React 框架用于快速构建 CRUD 应用管理面板、仪表盘、内部工具等其特点是 UI 无关可与 Ant Design、Mantine、Material UI 或任意自定义设计无缝集成、后端无关并内置了认证、状态管理、数据获取与路由等能力。headless 意味着它不提供预设 UI 组件你可以完全掌控应用外观使其符合团队的品牌与设计规范。本文示例的数据层正是通过 Refine 的useListHook 从 REST API 拉取再交给 React DND 做拖拽。项目准备初始化 Refine 应用使用 Refine CLI 创建项目npm create refine-applatest按如下选项选择即可生成带 CRUD 页面的基础工程✔ Choose a project template · Refine(Next.js) ✔ What would you like to name your project?: · refine-dnd ✔ Choose your backend service to connect: · REST API ✔ Do you want to use a UI Framework?: · Ant Design ✔ Do you want to add example pages?: · Yes ✔ Do you need any Authentication logic?: · No ✔ Do you need i18n (Internationalization) support?: · No ✔ Choose a package manager: · npm进入项目目录并安装 React DND 及其 HTML5 后端npm install react-dnd react-dnd-html5-backend启动开发服务器npm run dev浏览器会自动打开预览若未自动打开可手动访问http://localhost:3000。仓库中的完整示例工程 examples/blog-react-dnd/package.json 使用的依赖版本为react-dnd^16.0.1、react-dnd-html5-backend^16.0.1、refinedev/core^4.56.0、refinedev/antd^5.44.0、antd^5.23.0与react^19.1.0可作为版本对齐的参考。搭建 Dashboard 页面脚手架项目默认没有 Dashboard 页需要先手动创建。在pages目录下新建dashboardPage.tsximport { Typography } from antd; function DashBoardPage() { return TypographyThis is the Dashboard page/Typography; } export default DashBoardPage;然后在App.tsx中引入该页面注册路由与资源。核心改动有两点把DashboardPage /作为 index 路由元素并在Refine /的resources数组中添加dashboard资源list: /使其出现在侧边栏菜单中import DashBoardPage from ./pages/dashboardPage; // ... Refine dataProvider{dataProvider(https://api.fake-rest.refine.dev)} notificationProvider{useNotificationProvider} routerProvider{routerProvider} resources{[ { name: dashboard, list: /, }, { name: blog_posts, list: /blog-posts, create: /blog-posts/create, edit: /blog-posts/edit/:id, show: /blog-posts/show/:id, meta: { canDelete: true }, }, // ... ]} options{{ syncWithLocation: true, warnWhenUnsavedChanges: true }} Routes Route element{ ThemedLayout Header{() Header sticky /} Sider{(props) ThemedSider {...props} fixed /} Outlet / /ThemedLayout } Route index element{DashBoardPage /} / {/* ... 其他路由 */} /Route /Routes /Refine保存后点击侧边栏的Dashboard按钮即可进入该页面。完整的App.tsx结构可参考仓库中的 examples/blog-react-dnd/src/App.tsx。集成 React DNDDndProvider 与 HTML5Backend使用 React DND 前必须用DndProvider /包裹应用根组件并传入后端backend。回到App.tsx引入并包裹import { Refine } from refinedev/core; import { DndProvider } from react-dnd; import { HTML5Backend } from react-dnd-html5-backend; function App() { return ( DndProvider backend{HTML5Backend} Refine /* ... */ / /DndProvider ); } export default App;React DND 为触摸设备与非触摸设备分别提供独立后端。HTML5Backend基于 HTML5 拖拽 API 实现用于桌面端它实现最直接、在绝大多数现代浏览器上工作良好但同样受限于 HTML5 API 的浏览器差异与不一致性。若漏掉DndProvider整个拖拽将完全不生效详见后文“错误排查”。核心 Hook 速览useDrag 与 useDropuseDrag让元素可拖拽useDrag用于创建可被拖动的元素接收一个配置对象返回[收集到的状态, 拖拽源 ref]const [{ isDragging }, dragRef] useDrag({ item: { type: item, item: myitem }, begin: () { console.log(drag began); }, end: (dropResult) { console.log(drag end); }, collect: (monitor) ({ isDragging: monitor.isDragging(), }), });返回值第一项是当前拖拽状态对象第二项是要绑定到可拖拽元素上的 ref。item定义拖拽过程中可用的数据建议包含type属性以便与useDrop的accept属性匹配。collect是回调函数用于访问 drag-and-drop monitor拖拽监视器这里收集isDragging布尔值表示当前是否有元素正在被拖拽。begin与end分别在拖拽开始和结束时触发。useDrop创建可接收拖拽的放置区useDrop与useDrag配合使用用于创建能接收拖入元素的放置目标const [{ isOver, canDrop }, dropRef] useDrop({ accept: item, drop: (item, monitor) { console.log(Dropped item: ${JSON.stringify(item)}); }, collect: (monitor) ({ isOver: monitor.isOver(), canDrop: monitor.canDrop(), }), });accept声明该放置区接受的元素类型必须与useDrag中的type一致本例均为item。当拖拽元素悬停于目标上方时isOver与canDrop为true元素被放下时调用drop函数入参是被拖拽的 item。看板数据层枚举、模型与 useData Hook开始写组件前先准备“常量”数据层位于src/components/constants/下。enums.ts定义列类型与卡片类型export enum ColumnTypes { ORDERS Orders, IN_PROGRESS In Progress, DELIVERED Delivered, RETURNED Returned, } export enum cardType { ORDER Order, }看板包含四列待处理订单Orders、进行中In Progress、已交付Delivered与已退回Returned每列都是可放置区接受的卡片类型为Order。models.ts定义数据接口import { ColumnTypes } from ./enums; export interface OrderProps { id: number; title: string; desc: string; column: ColumnTypes; } export interface dragItem { index: number; id: OrderProps[id]; } export interface IProduct { id: number; name: string; material: string; column?: ColumnTypes.ORDERS; }OrderProps描述每个订单的属性dragItem描述被拖拽的条目IProduct是商品数据模型用于对接数据提供者返回的数据。仓库中该文件的实际实现见 examples/blog-react-dnd/src/components/constants/models.ts。useData.ts用 Refine useList 拉取并改写数据import React from react; import { ColumnTypes } from ./enums; import { IProduct } from components/constants/models; import { useList } from refinedev/core; function useData() { // 使用 Refine 的 useList Hook 从 products 端点获取数据 const { result: { data }, } useListIProduct({ config: { pagination: { currentPage: 2, }, }, resource: products, }); // 改写数据为每个条目追加 column 属性 const newArr data?.map((i: IProduct) { return { ...i, column: ColumnTypes.ORDERS, }; }); return [newArr, data?.data]; } export default useData;useList是 Refine 提供的列表数据 Hook这里请求的是Refine /上dataProvider指向的 fake REST API 的products端点并显式翻到第 2 页currentPage: 2。从实现上讲这与直接执行fetch(https://api.fake-rest.refine.dev/products)获取数据等价但由 Refine 数据提供者统一处理请求、缓存与状态。map遍历返回数据为每个对象追加column: ColumnTypes.ORDERS。column属性决定卡片归属于哪一列当前所有条目都被标记为Orders的子项后续拖拽时通过修改该字段实现列间移动。函数返回新旧两份数组newArr带 column 字段与data?.data原始数据后者用于触发副作用更新。看板布局Column 与 Cards 组件Column 组件可复用的列容器import React from react; import { Row, Col } from antd; function Column({ children, name }: { children: any; name: string }) { return ( Row gutter{30} Col style{{ backgroundColor: #e3e7ee, width: 270px, padding: 15px, minHeight: 170px, maxHeight: 690px, borderRadius: 5px, overflowY: scroll, }} div style{{ fontSize: 17px, marginLeft: 10px, marginBottom: 15px, color: #84878c, }} {name} /div div style{{ width: 100%, height: 75%, padding: 4px, }} {children} /div /Col /Row ); } export default Column;该组件接收children列内卡片与name列标题两个 props使用 Ant Design 的Row/Col声明式布局。固定宽度 270px、可滚动区域便于重复渲染四列。DashboardPage渲染四列并过滤卡片import React from react; import Column from ../components/columns; import { ColumnTypes } from ../components/constants/enums; import { Space } from antd; function DashboardPage() { const { ORDERS, IN_PROGRESS, DELIVERED, RETURNED } ColumnTypes; return ( div Space directionhorizontal alignbaseline size{109} style{{ display: flex, justifyContent: center, marginTop: 20px, }} Column name{ORDERS}{}/Column Column name{IN_PROGRESS}{}/Column Column name{DELIVERED}{}/Column Column name{RETURNED}{}/Column /Space /div ); } export default DashboardPage;通过ColumnTypes枚举把四列渲染到画布上。此时列内尚无卡片内容。渲染卡片useState useEffect 联动数据import React, { useEffect, useState } from react; import { Space } from antd; import Column from ../components/columns; import Cards from ../components/cards; import { ColumnTypes } from ../components/constants/enums; import useData from ../components/constants/useData; function DashboardPage() { const [newArr, products] useData(); const [orders, setOrders] useStateany[] | undefined([]); useEffect(() { setOrders(newArr); }, [products]); const columnItem (columnName: string) { return ( orders orders .filter((order) order.column columnName) .map((order, index) ( Cards key{order.id} name{order.name} material{order.material} setOrders{setOrders} index{index} / )) ); }; // ... 渲染四列时传入 columnItem(ORDERS) 等 }useData返回newArr与productsuseState初始化orders为空数组useEffect在products变化时把newArr写入状态依赖项用原始products触发避免引用不稳定造成死循环。columnItem(columnName)先按column字段过滤卡片再映射为Cards /。由于目前所有条目column均为Orders卡片只会出现在 Orders 列。最后把columnItem作为每列的 children 传入Column name{ORDERS}{columnItem(ORDERS)}/Column Column name{IN_PROGRESS}{columnItem(IN_PROGRESS)}/Column Column name{DELIVERED}{columnItem(DELIVERED)}/Column Column name{RETURNED}{columnItem(RETURNED)}/ColumnCards 组件基础版import React from react; import { Card } from antd; function Cards({ name, material, setOrders, }: { name: string; material: string; setOrders: any; index: number; }) { return ( Card title{name} classNamecard style{{ marginBottom: 15px, boxShadow: 1px 4px 11px -2px rgba(135,135,135,0.75), }} {material} /Card ); } export default Cards;这是最简单的卡片展示组件接收name、material等 props 并用 Ant Design 的Card渲染。仓库中的最终实现见 examples/blog-react-dnd/src/components/cards.tsx。让卡片可拖拽接入 useDrag在Cards组件内声明useDrag指定卡片类型与 item 数据并把返回的dragref 绑定到Card上import React from react; import { Card } from antd; import { cardType } from ./constants/enums; import { useDrag } from react-dnd; function Cards({ name, material, setOrders }: { /* ... */ }) { const [{ isDragging }, drag] useDrag({ type: cardType.ORDER, item: { name }, collect: (monitor) ({ isDragging: monitor.isDragging(), }), }); return ( Card ref{drag} title{name} classNamecard style{{ opacity: isDragging ? 0.5 : 1, marginBottom: 15px, boxShadow: 1px 4px 11px -2px rgba(135,135,135,0.75), }} {material} /Card ); } export default Cards;type: cardType.ORDER使用枚举统一类型标识。collect回调通过 monitor 的isDragging()判断卡片是否正在被拖拽并据此把卡片透明度调整为0.5拖拽中或1正常。完成这一步后浏览器中的卡片即可被拖起。让列可接收拖放接入 useDrop放置区位于每一列因此在Column组件内声明useDropimport React from react; import { Row, Col } from antd; import { useDrop } from react-dnd; import { cardType } from ./constants/enums; function Column({ children, name }: { children: any; name: string }) { const [{ canDrop, isOver }, dropref] useDrop({ accept: cardType.ORDER, drop: () ({ name, }), collect: (monitor) ({ isOver: monitor.isOver(), canDrop: monitor.canDrop(), }), }); return ( Row gutter{30} Col style{{ backgroundColor: #e3e7ee, width: 270px, padding: 15px, minHeight: 170px, maxHeight: 690px, borderRadius: 5px, overflowY: scroll, }} div style{{ fontSize: 17px, marginLeft: 10px, marginBottom: 15px, color: #84878c, }} {name} /div div ref{dropref} style{{ width: 100%, height: 75%, padding: 4px, border: isOver ? dashed 1px black : , }} {children} /div /Col /Row ); } export default Column;accept: cardType.ORDER与useDrag的type保持一致。drop回调返回{ name }——即当前列的名字供拖拽源在end阶段读取通过monitor.getDropResult()。collect收集isOver与canDrop当卡片悬停在放置区上方时二者为true。此时把放置区的边框改为虚线dashed 1px black作为视觉反馈。类型匹配是关键useDrag与useDrop共享同一类型Order所以悬停时isOver/canDrop才会返回true。这也解释了为什么useDrop的accept必须与useDrag的type完全一致。实现列间移动orderColumnChange 与 end 回调目前放下卡片还不会改变位置。需要在Cards组件中加入排序函数并在useDrag的end回调里根据落点更新列归属。先添加状态更新函数const orderColumnChange (CurrentOrder: any, columnName: string) { setOrders((prevState: string[]) { return prevState.map((item: any) { return { ...item, column: item.name CurrentOrder.name ? columnName : item.column, }; }); }); };它遍历前一个状态把“名称匹配当前拖拽卡片”的条目的column改为目标列名从而让该卡片成为目标列的子项。再在useDrag中补充end回调根据monitor.getDropResult()返回的列名分派到对应列import { ColumnTypes, cardType } from ./constants/enums; import { useDrag } from react-dnd; import { IProduct } from ./constants/models; const [{ isDragging }, drag] useDrag({ type: cardType.ORDER, item: { name }, end: (order, monitor) { const dropResult monitor.getDropResultIProduct(); if (dropResult) { const { name } dropResult; const { ORDERS, IN_PROGRESS, DELIVERED, RETURNED } ColumnTypes; switch (name) { case ORDERS: orderColumnChange(order, ColumnTypes.ORDERS); break; case IN_PROGRESS: orderColumnChange(order, ColumnTypes.IN_PROGRESS); break; case DELIVERED: orderColumnChange(order, ColumnTypes.DELIVERED); break; case RETURNED: orderColumnChange(order, ColumnTypes.RETURNED); break; default: break; } } }, collect: (monitor) ({ isDragging: monitor.isDragging(), }), });至此卡片可以被拖放到任意一列且状态会实时更新、重渲染到对应列中。仓库中cards.tsx、columns.tsx的最终代码与本文一致可直接对照查看 examples/blog-react-dnd/src/components/cards.tsx 与 examples/blog-react-dnd/src/components/columns.tsx。性能优化让大量可拖拽项保持流畅当可拖拽项数量增大时性能可能成为瓶颈。以下是实用策略避免不必要的重渲染利用React.memo或useMemo缓存不变的拖拽项。例如可拖拽条目本身未变化时对其做 memo 化可节省大量处理时间。批量更新状态一次 drop 需要更新多个条目时尽量合并为一次状态更新避免多次触发 React 渲染周期如上面的orderColumnChange就是一次性map出全新数组后调用一次setOrders。收窄 collect 函数的范围useDrag/useDrop的collect很强大但塞入过多逻辑会拖慢性能只收集必要的数据即可。虚拟化大列表面对成百上千个可拖拽项可引入react-window或react-virtualized之类的虚拟列表库只渲染可见项显著降低 React 的渲染负担。错误排查与调试常见问题与对策缺少 DndProvider如果忘记用DndProvider包裹应用拖拽功能完全不工作。确保根组件包含它import { DndProvider } from react-dnd; import { HTML5Backend } from react-dnd-html5-backend; function App() { return ( DndProvider backend{HTML5Backend} YourApp / /DndProvider ); }useDrag 与 useDrop 类型不匹配useDrag的type必须与useDrop的accept一致否则放置区无法识别被拖拽的条目const [{ isDragging }, dragRef] useDrag({ type: item }); const [{ isOver }, dropRef] useDrop({ accept: item });Monitor 数据不更新如果collect拿不到准确数据确认你从 monitor 返回了正确的取值collect: (monitor) ({ isDragging: monitor.isDragging() });这类小问题排查起来很耗时建议结合浏览器控制台的详细报错信息与 React DND 官方文档对照处理。移动端支持切换到 Touch Backend默认的HTML5Backend在桌面浏览器表现良好但在移动设备上并不理想。React DND 为触摸屏提供了独立的 Touch Backendnpm install react-dnd-touch-backend然后在DndProvider中把HTML5Backend替换为TouchBackendimport { DndProvider } from react-dnd; import { TouchBackend } from react-dnd-touch-backend; function App() { return ( DndProvider backend{TouchBackend} YourApp / /DndProvider ); }还可以通过options定制触摸行为例如调整拖拽延迟const backendOptions { enableMouseEvents: true, delay: 100, }; DndProvider backend{TouchBackend} options{backendOptions} YourApp / /DndProvider;enableMouseEvents: true让鼠标事件也可用便于在同时支持触控与鼠标的设备上调试delay控制触摸按下到开始拖拽的等待毫秒数可避免与页面滚动等手势冲突。经此调整拖拽功能即可在桌面端与移动端同时工作。总结本文完成了从零到一的全流程通过 Refine CLI 初始化带 CRUD 页面的工程集成 React DND 的DndProvider与HTML5Backend利用useList拉取数据并渲染出包含四列的看板通过useDrag让卡片可拖拽、useDrop让列成为放置区并用end回调 状态更新实现列间移动。随后补充了性能优化、常见错误排查与移动端 Touch Backend 的支持方案。在 Refine 生态中React DND 这类 headless 集成方式与框架本身的“UI 无关”设计高度契合数据层交给 Refine 的 data provider 与 Hooks交互层交给 React DND而视觉层完全由你掌控。完整的可运行示例含App.tsx、cards.tsx、columns.tsx、enums.ts、models.ts、useData.ts及页面入口可在仓库 examples/blog-react-dnd 中直接查看、安装依赖并运行体验。【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表