使用 React 和拖放功能创建应用程序,无需库 👆!
使用 React 和拖放功能创建应用程序,无需库 👆!
拖放式应用程序如今非常普遍,它们极大地提升了应用程序的用户体验。**您或许也想在下一个项目中实现它。
这次,我将向你展示如何创建一个具有拖放功能的应用程序,但无需使用任何外部库,仅使用 React JS。
🚨 注意:本文要求您了解React 与 TypeScript 的基础知识(基本 hooks 和自定义 hooks)。
任何形式的反馈都非常欢迎,谢谢!希望您喜欢这篇文章。🤗
目录。
📌将要使用的技术。📌 创建项目。📌
第一步。📌 创建卡片。📌 创建卡片容器。
📌定义卡片信息的类型和界面。
📌创建 DragAndDrop.tsx 组件。📌
添加一些数据以创建卡片。📌显示一些卡片。📌
执行 拖动操作。📌 执行放置操作。
📌创建用于存放卡片 的状态。📌执行将卡片放入容器中的函数。
📌可选。重构代码,见DragAndDrop.tsx
📌结论。
📌在线演示。📌
源代码。
👉 将要使用的技术。
- ▶️ React JS (v.18)
- ▶️ Vite JS
- ▶️ TypeScript
- ▶️ 原生 CSS(您可以在本文末尾的仓库中找到样式)
👉 创建项目。
我们将为项目命名:(dnd-app可选,您可以随意命名)。
npm init vite@latest
我们使用 Vite JS 创建项目,并选择 React 和 TypeScript。
然后运行以下命令导航到刚刚创建的目录。
cd dnd-app
然后我们安装依赖项。
npm install
然后我们在代码编辑器(我使用的是 VS Code)中打开项目。
code .
然后通过这条命令启动开发服务器,最后打开浏览器访问http://localhost:5173(在 Vite 版本 2 中端口号为localhost:3000,但在新版本中端口号为localhost:5173)。
npm run dev
👉 第一步。
我们立即创建文件夹src/components,并在其中添加文件Title.tsx,然后在文件中添加:
export const Title = () => {
return (
<div className="title flex">
<h1>Creating basic Drag & Drop 👆 </h1>
<span>( without external libraries )</span>
</div>
)
}
现在,在文件内部src/App.tsx,我们删除文件的所有内容,并放置一个功能组件来显示我们刚刚创建的标题。
import { Title } from "./components/Title"
const App = () => {
return (
<div className="container-main flex">
<Title />
</div>
)
}
export default App
它应该看起来像这样👀:

👉 创建我们的卡片。
在文件夹内src/components添加文件CardItem.tsx。
目前它不会收到任何道具,稍后会收到。
export const CardItem = () => {
return (
<div className='card-container'>
<p>content</p>
</div>
)
}
我们目前还不会在文件中使用 Card 组件,但如果您愿意,可以将其导入到src/App.tsx文件中,以便设置其样式并在屏幕上查看。
👉 为我们的卡片创建容器。
现在我们来创建卡片容器。
在文件夹内src/components,我们添加文件ContainerCards.tsx并添加以下内容:
目前,该组件接收状态作为参数(您可以查看状态的类型)。
import { Status } from '../interfaces'
interface Props {
status: Status
}
export const ContainerCards = ({ status }: Props) => {
return (
<div className="layout-cards" >
<p>{status} hero</p>
{/* Cards */}
</div>
)
}
🟠 定义卡片信息的类型和界面。
状态类型如下:
export type Status = 'good' | 'bad' | 'normal'
这种类型位于src/interfaces文件夹内的某个文件中index.ts(必须创建该文件,因为状态类型将在多个文件中使用)。
创建时还要添加以下接口index.ts。src/interfaces
这就是卡片数据的显示方式。
export interface Data {
id: number
content: string
status: Status
}
👉 创建 DragAndDrop.tsx 组件。
目前我们已经创建了用于存放卡片的组件,但我们需要 3 个卡片容器:
- 献给正义的英雄。
- 献给平凡英雄。
- 献给那些邪恶的英雄。
在文件夹内src/components,我们添加该文件DragAndDrop.tsx并添加以下内容:
import { Status } from "../interfaces"
import { ContainerCards } from "./ContainerCards"
const typesHero: Status[] = ['good', 'normal', 'bad']
export const DragAndDrop = () => {
return (
<div className="grid">
{
typesHero.map( container => (
<ContainerCards
status={container}
key={container}
/>
))
}
</div>
)
}
必须将此组件添加到src/App.tsx。
import { DragAndDrop} from "./components/DragAndDrop"
import { Title } from "./components/Title"
const App = () => {
return (
<div className="container-main flex">
<Title />
<DragAndDrop />
</div>
)
}
export default App
目前大概是这个样子👀……

现在我们有了可以放置和整理卡片的容器。👋
现在我们需要制作一些卡片。
👉 添加一些数据以创建卡片。
现在我们创建一个文件夹src/assets,并在其中创建一个文件index.ts,该文件将包含一个列表,其中包含要填写到卡片中的数据。
import { Data } from "../interfaces";
export const data: Data[] = [
{
id: 1,
content: 'Aqua-man',
status: 'good'
},
{
id: 2,
content: 'Flash',
status: 'normal'
},
{
id: 3,
content: 'Green Lantern',
status: 'good'
},
{
id: 4,
content: 'Batman',
status: 'bad'
},
]
现在,在返回的ContainerCardssrc/components/DragAndDrop.tsx组件中,我们传递一个名为items 的新属性,并将我们在文件夹中创建的数据作为值传递给该属性。src/assets
import { ContainerCards } from "./ContainerCards"
import { Status } from "../interfaces"
import { data } from "../assets"
const typesHero: Status[] = ['good', 'normal', 'bad']
export const DragAndDrop = () => {
return (
<div className="grid">
{
typesHero.map( container => (
<ContainerCards
status={container}
key={container}
items={data}
/>
))
}
</div>
)
}
这将抛出一个错误,因为items不是 ContainerCards 期望的属性。😥
但我们会在下一节中解决这个问题。👇
👉 展示一些牌。
为了显示一些卡片,我们需要对每个组件的参数进行一些更改。
1 - 首先是组件src/components/CardItem.tsx。
它将接收类型为 Data 的数据作为 props,也就是我们之前定义的数据。
我们立即显示数据中的属性内容。
import { Data } from "../interfaces"
interface Props {
data: Data
}
export const CardItem = ({ data, handleDragging }: Props) => {
return (
<div className='card-container'>
<p>{data.content}</p>
</div>
)
}
2 - 在组件中,我们通过添加 items 属性(一个数据列表)并对其进行解构src/components/ContainerCards.tsx来更改Props接口。
import { Data, Status } from "../interfaces"
interface Props {
items: Data[]
status: Status
}
export const ContainerCards = ({ items = [], status }: Props) => {
return (
<div className="layout-cards">
<p>{status} hero</p>
</div>
)
}
然后,在标签下,我们对项目p进行迭代。最后,我们将结果传递给CardItem的属性 并返回。
CardItem.tsxitemdata
import { Data, Status } from "../interfaces"
import { CardItem } from "./CardItem"
interface Props {
items: Data[]
status: Status
}
export const ContainerCards = ({ items = [], status}: Props) => {
return (
<div className="layout-cards">
<p>{status} hero</p>
{
items.map(item => (
<CardItem
data={item}
key={item.id}
/>
))
}
</div>
)
}
这将向您发出警告,提示您按键重复😥。
这是因为我们渲染了 3 倍的 ContainerCards。
但是等等,唯一能区分这 3 个组件的属性是状态。
因此,我们将设定以下条件:
- 如果ContainerCards 组件接收到的状态与项目的状态(即超级英雄的状态)相等,则渲染该项目,否则返回 false。
import { Data, Status } from "../interfaces"
import { CardItem } from "./CardItem"
interface Props {
items: Data[]
status: Status
}
export const ContainerCards = ({ items = [], status }: Props) => {
return (
<div className="layout-cards">
<p>{status} hero</p>
{
items.map(item => (
status === item.status
&& <CardItem
data={item}
key={item.id}
/>
))
}
</div>
)
}
这样我们就避免了钥匙冲突,卡片将按以下方式排序👀……

👉 表演拖拽动作。
为了实现拖拽功能,我们首先在 中定义一个状态和一个函数src/components/DragAndDrop.tsx。
我们将其作为 prop 传递给ContainerCards组件:
- isDragging,它将具有状态的值。
- handleDragging是我们创建的用于更新状态的函数。
import { ContainerCards } from "./ContainerCards"
import { data } from "../assets"
import { Status } from "../interfaces"
const typesHero: Status[] = ['good', 'normal', 'bad']
export const DragAndDrop = () => {
const [isDragging, setIsDragging] = useState(false)
const handleDragging = (dragging: boolean) => setIsDragging(dragging)
return (
<div className="grid">
{
typesHero.map(container => (
<ContainerCards
items={data}
status={container}
key={container}
isDragging={isDragging}
handleDragging={handleDragging}
/>
))
}
</div>
)
}
这将失败,因为ContainerCards不接受这些属性。
因此,我们需要更改ContainerCards的接口。
该文件src/components/ContainerCards.tsx。
interface Props {
items: Data[]
status: Status
isDragging: boolean
handleDragging: (dragging: boolean) => void
}
我们一下子就拿到了那些道具。
import { CardItem } from "./CardItem"
import { Data, Status } from "../interfaces"
interface Props {
items: Data[]
status: Status
isDragging: boolean
handleDragging: (dragging: boolean) => void
}
export const ContainerCards = ({ items = [], status, isDragging, handleDragging }: Props) => {
return (
<div
className={`layout-cards ${isDragging ? 'layout-dragging' : ''}`}
>
<p>{status} hero</p>
{
items.map(item => (
status === item.status
&& <CardItem
data={item}
key={item.id}
handleDragging={handleDragging}
/>
))
}
</div>
)
}
CardItem会报错,因为它不接受handleDragging属性,所以我们必须修改它的接口。
现在我们在文件中src/components/CardItem.tsx修改接口
interface Props {
data: Data,
handleDragging: (dragging: boolean) => void
}
现在,我们开始为这个组件添加拖拽功能。
首先,我们给整个卡片添加draggablediv属性,表明该组件可以拖动。
import { Data } from "../interfaces"
interface Props {
data: Data,
handleDragging: (dragging: boolean) => void
}
export const CardItem = ({ data, handleDragging }: Props) => {
return (
<div
className='card-container'
draggable
>
<p>{data.content}</p>
</div>
)
}
然后我们添加onDragEnd属性,该属性将执行handleDragEnd函数。
此函数只会将 isDragging 状态的值设置为 false,因为当 onDragEnd 执行时,卡片将不再被拖动,所以我们必须移除拖动时的样式,即恢复所有样式,就像开始时一样。
import { Data } from "../interfaces"
interface Props {
data: Data,
handleDragging: (dragging: boolean) => void
}
export const CardItem = ({ data, handleDragging }: Props) => {
const handleDragEnd = () => handleDragging(false)
return (
<div
className='card-container'
draggable
onDragEnd={handleDragEnd}
>
<p>{data.content}</p>
</div>
)
}
然后我们添加onDragStart属性(当组件开始拖动时执行,如果我们没有添加draggable属性,则不会执行onDragStart )。
onDragStart会执行handleDragStart函数。
该函数接收事件,事件中有一个我们感兴趣的属性,即dataTransfer。
dataTransfer属性允许我们在拖动元素时包含或获取数据。
dataTransfer中的 setData 属性用于设置拖动元素时要包含的数据,它接收两个参数:
注意:dataTransfer 类中还有一个名为clearData 的属性,用于清除我们存储的数据缓存。在本例中,由于我们将覆盖相同的标识符“text”,因此无需执行此操作。
在包含数据之后,我们执行 handleDragging 函数,并将 true 值发送给用户,以表明我们正在拖动一个元素。
import { Data } from "../interfaces"
interface Props {
data: Data,
handleDragging: (dragging: boolean) => void
}
export const CardItem = ({ data, handleDragging }: Props) => {
const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
e.dataTransfer.setData('text', `${data.id}`)
handleDragging(true)
}
const handleDragEnd = () => handleDragging(false)
return (
<div
className='card-container'
draggable
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<p>{data.content}</p>
</div>
)
}
这样,在拖动元素时,我们已经准备好了其中包含的信息,当我们将其放入另一个容器中时,就可以获取这些信息。
这就是拖动卡片时的样子,它会改变容器的设计,表明这些容器是你可以放置卡片的位置。

👉 进行 Drop 动作。
在执行释放元素的操作之前,我们必须先完成其他一些事情。
🟠 创造掌握主动权的局面。
首先,为了建立一个处于某种状态的英雄列表,并且能够在卡牌被放入另一个容器时更新它,此时我们会更新英雄的状态属性,这将导致列表重新渲染,并对已更改的卡牌进行排序。
为此,我们进入src/components/DragAndDrop.tsx并创建一个新状态。
它的初始值将是我们之前在 中定义的数据src/assets。
import { data } from "../assets"
const [listItems, setListItems] = useState<Data[]>(data)
现在,在渲染ContainerCards组件时,我们将不再把数据值传递给items属性,而是把listItems状态的值传递给它。
import { ContainerCards } from "./ContainerCards"
import { data } from "../assets"
import { Status } from "../interfaces"
const typesHero: Status[] = ['good', 'normal', 'bad']
export const DragAndDrop = () => {
const [isDragging, setIsDragging] = useState(false)
const [listItems, setListItems] = useState<Data[]>(data)
const handleDragging = (dragging: boolean) => setIsDragging(dragging)
return (
<div className="grid">
{
typesHero.map(container => (
<ContainerCards
items={listItems}
status={container}
key={container}
isDragging={isDragging}
handleDragging={handleDragging}
/>
))
}
</div>
)
}
接下来,我们将创建一个函数来更新列表项的状态。
我们将其命名为handleUpdateList,它将接收两个参数:
- id:卡片的标识符,类型为数字。
- 状态:卡片的新状态,类型为状态。
函数内部……
1 - 首先,我们将通过 ID 在 listItems 状态值中查找元素。
2 - 我们将评估数据是否存在,如果传递给我们的状态与它已有的状态不同,我们将更改状态。
3 - 在条件中,我们访问找到的卡片,并将更新其状态属性,将其赋值为通过函数中的参数传递给我们的新状态。
4 - 我们调用 setListItems 来更新状态,放置:
现在,我们向 ContainerCards 组件添加一个名为handleUpdateList的新属性,并将我们刚刚创建的函数handleUpdateList传递给它。
import { ContainerCards } from "./ContainerCards"
import { data } from "../assets"
import { Status } from "../interfaces"
const typesHero: Status[] = ['good', 'normal', 'bad']
export const DragAndDrop = () => {
const [isDragging, setIsDragging] = useState(false)
const [listItems, setListItems] = useState<Data[]>(data)
const handleDragging = (dragging: boolean) => setIsDragging(dragging)
const handleUpdateList = (id: number, status: Status) => {
let card = listItems.find(item => item.id === id)
if (card && card.status !== status) {
card.status = status
setListItems( prev => ([
card!,
...prev.filter(item => item.id !== id)
]))
}
}
return (
<div className="grid">
{
typesHero.map(container => (
<ContainerCards
items={listItems}
status={container}
key={container}
isDragging={isDragging}
handleDragging={handleDragging}
handleUpdateList={handleUpdateList}
/>
))
}
</div>
)
}
这将导致错误,因为ContainerCards组件不接受handleUpdateList属性, 所以我们必须更新ContainerCards接口。
src/components/ContainerCards.tsx
interface Props {
items: Data[]
status: Status
isDragging: boolean
handleDragging: (dragging: boolean) => void
handleUpdateList: (id: number, status: Status) => void
}
👉 执行将物品放入容器中的操作。
我们已进入src/components/ContainerCards.tsx。
在组件内部,我们将为div元素设置两个新属性。
<div
className={`layout-cards ${isDragging ? 'layout-dragging' : ''}`}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
<p>{status} hero</p>
{
items.map(item => (
status === item.status
&& <CardItem
data={item}
key={item.id}
handleDragging={handleDragging}
/>
))
}
</div>
handleDragOver 函数只会执行此操作。首先,它会接收onDragOver
事件。 由于默认情况下数据无法拖放到其他元素上,为了允许拖放,我们必须避免这种默认行为。
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
}
现在是handleDrop函数
-
首先,您将收到发出onDrop 的事件。
-
在函数内部,我们避免了默认行为,这在处理图像时更为明显(当我们在应用程序的某个位置拖放图像时,它会打开图像,将我们带出应用程序)。
-
然后,从事件中,我们获取 dataTransfer 属性,并通过 dataTransfer 的 getData 属性执行它,发送标识符,从中我们将获得卡的 ID。
+开头的符号用于e.dataTransfer.getData('text')将值转换为数字。
-
然后我们将调用组件通过 props 传递给我们的handleUpdateList函数(我们必须将其从组件中解构出来)。
- 首先,我们将从dataTransfer的getData属性中获取的 id 传递给它,该 id已经转换为数字。
- 然后我们将组件中通过 props 接收到的状态传递给它。
-
最后,我们调用handleDragging函数,并将值 false 传递给用户,以表明我们不再拖动任何内容。
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
const id = +e.dataTransfer.getData('text')
handleUpdateList(id, status)
handleDragging(false)
}
以下是代码示例src/components/ContainerCards.tsx:
import { Data, Status } from "../interfaces"
import { CardItem } from "./CardItem"
interface Props {
items: Data[]
status: Status
isDragging: boolean
handleUpdateList: (id: number, status: Status) => void
handleDragging: (dragging: boolean) => void
}
export const ContainerCards = ({ items = [], status, isDragging, handleDragging, handleUpdateList }: Props) => {
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
handleUpdateList(+e.dataTransfer.getData('text'), status)
handleDragging(false)
}
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => e.preventDefault()
return (
<div
className={`layout-cards ${isDragging ? 'layout-dragging' : ''}`}
onDrop={handleDrop}
onDragOver={handleDragOver}
>
<p>{status} hero</p>
{
items.map(item => (
status === item.status
&& <CardItem
data={item}
key={item.id}
handleDragging={handleDragging}
/>
))
}
</div>
)
}
最终效果应该像这样🥳!

👉 可选。重构代码DragAndDrop.tsx
我们的组件中已经包含了足够的逻辑,因此创建一个自定义钩子来管理这些逻辑是一个不错的选择。
我们创建一个src/hooks文件夹,并在其中创建一个名为“.”的文件useDragAndDrop.ts。
首先我们定义该函数,它将接收一个类型为数据数组的初始状态。
export const useDragAndDrop = (initialState: Data[]) => {}
我们从DragAndDrop.tsx组件中提取所有逻辑,并将其放入自定义钩子中。
listItems状态的初始值将是钩子函数参数传递给我们的值。
最后,我们以对象的形式返回:
import { useState } from "react"
import { Data, Status } from "../interfaces"
export const useDragAndDrop = (initialState: Data[]) => {
const [isDragging, setIsDragging] = useState(false)
const [listItems, setListItems] = useState<Data[]>(initialState)
const handleUpdateList = (id: number, status: Status) => {
let card = listItems.find(item => item.id === id)
if (card && card.status !== status) {
card.status = status
setListItems( prev => ([
card!,
...prev.filter(item => item.id !== id)
]))
}
}
const handleDragging = (dragging: boolean) => setIsDragging(dragging)
return {
isDragging,
listItems,
handleUpdateList,
handleDragging,
}
}
现在,在组件中src/components/DragAndDrop.tsx,我们调用自定义钩子。
我们将数据通过参数发送到我们的钩子函数,然后我们只需对属性进行反结构化处理,就完成了。
import { ContainerCards } from "./ContainerCards"
import { useDragAndDrop } from "../hooks/useDragAndDrop"
import { Status } from "../interfaces"
import { data } from "../assets"
const typesHero: Status[] = ['good', 'normal', 'bad']
export const DragAndDrop = () => {
const { isDragging, listItems, handleDragging, handleUpdateList } = useDragAndDrop(data)
return (
<div className="grid">
{
typesHero.map(container => (
<ContainerCards
items={listItems}
status={container}
key={container}
isDragging={isDragging}
handleDragging={handleDragging}
handleUpdateList={handleUpdateList}
/>
))
}
</div>
)
}
这将使你的组件更易于阅读。🎉
👉 结论。
这是在不使用外部库的情况下构建具有拖放功能的应用程序的方法之一。
希望我的讲解能帮助你理解如何进行这项练习,非常感谢你坚持看到这里!🤗❤️
如果您觉得这篇文章有用或有趣,或者您知道其他更好或不同的拖放方法,欢迎留言评论。🙌
🟠 在线演示。
https://drag-and-drop-react-app.netlify.app
🟠 源代码。
使用 React 和拖放功能创建应用程序,无需库 👆!
这次,我们将使用 React JS 实现拖放功能,而无需任何其他外部包或库!

特点⚙️
- 卡片拖拽。
- 将卡片放入容器中。
- 卡片分类。
科技🧪
- React JS
- TypeScript
- Vite JS
- 原生 CSS 3
安装🧰
- 克隆仓库(需要安装Git)。
git clone https://github.com/Franklin361/drag-and-drop-react
- 安装项目依赖项。
npm install
- 运行项目。
npm run dev
链接⛓️
应用演示 🔥
这是教程的链接,如果你想看看的话!👀
文章来源:https://dev.to/franklin030601/creating-an-app-using-drag-and-drop-with-react-without-libraries--5cg9