React實(shí)現(xiàn)卡片拖拽效果流程詳解
前提摘要:
學(xué)習(xí)宋一瑋 React 新版本 + 函數(shù)組件 &Hooks 優(yōu)先 開篇就是函數(shù)組件+Hooks 實(shí)現(xiàn)的效果如下: 學(xué)到第11篇了 照葫蘆畫瓢,不過老師在講解的過程中沒有考慮拖拽目標(biāo)項(xiàng)邊界問題,我稍微處理了下這樣就實(shí)現(xiàn)拖拽流暢了

下面就是主要的代碼了,實(shí)現(xiàn)拖拽(src/App.js):
核心在于標(biāo)記當(dāng)前項(xiàng),來源項(xiàng),目標(biāo)項(xiàng),并且在拖拽完成時(shí)對(duì)數(shù)據(jù)處理,更新每一組數(shù)據(jù)(useState);
/** @jsxImportSource @emotion/react */
// 上面代碼是使用emotion的關(guān)鍵CSS-in-JS
import React, { useEffect, useState, useRef } from "react";
import { css } from "@emotion/react";
import "./App.css";
const MINUTE = 60 * 1000;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
const UPDATE_INTERVAL = MINUTE;
// const ongoingList = [{ title: "進(jìn)行任務(wù)", status: "2022-11-09 15:29" }];
// const doneList = [{ title: "完成任務(wù)", status: "2022-11-09 15:59" }];
const KanbanBoard = ({ children }) => (
<main
css={css`
flex: 10;
display: flex;
flex-direction: row;
gap: 1rem;
margin: 0 1rem 1rem;
`}
>
{children}
</main>
);
const KanbanColumn = ({
children,
className,
title,
setIsDragSource = () => { },
setIsDragTarget = () => { },
onDrop,
}) => {
const combinedClassName = `kanban-column ${className}`;
return (
<section
className={combinedClassName}
onDragStart={() => setIsDragSource(true)}
onDragOver={(evt) => {
evt.preventDefault();
evt.dataTransfer.dropEffect = "move";
setIsDragTarget(true);
}}
onDragLeave={(evt) => {
evt.preventDefault();
evt.dataTransfer.dropEffect = "none";
setIsDragTarget(false);
}}
onDrop={(evt) => {
evt.preventDefault();
onDrop && onDrop(evt);
}}
onDragEnd={(evt) => {
evt.preventDefault();
setIsDragSource(false);
setIsDragTarget(false);
}}
>
<h2>{title}</h2>
<ul>{children}</ul>
</section>
);
};
const KanbanCard = ({ title, status, onDragStart }) => {
const [displayTime, setDisplayTime] = useState(status);
useEffect(() => {
const updateDisplayTime = () => {
const timePassed = new Date() - new Date(status);
let relativeTime = "剛剛";
if (MINUTE <= timePassed && timePassed < HOUR) {
relativeTime = `${Math.ceil(timePassed / MINUTE)} 分鐘前`;
} else if (HOUR <= timePassed && timePassed < DAY) {
relativeTime = `${Math.ceil(timePassed / HOUR)} 小時(shí)前`;
} else if (DAY <= timePassed) {
relativeTime = `${Math.ceil(timePassed / DAY)} 天前`;
}
setDisplayTime(relativeTime);
};
const intervalId = setInterval(updateDisplayTime, UPDATE_INTERVAL);
updateDisplayTime();
return function cleanup() {
clearInterval(intervalId);
};
}, [status]);
const handleDragStart = (evt) => {
evt.dataTransfer.effectAllowed = "move";
evt.dataTransfer.setData("text/plain", title);
onDragStart && onDragStart(evt);
};
return (
<li className="kanban-card" draggable onDragStart={handleDragStart}>
<div className="card-title">{title}</div>
<div className="card-status">{displayTime}</div>
</li>
);
};
const AddKanbanCard = ({ onSubmit }) => {
const [title, setTitle] = useState("");
const handleChange = (evt) => {
setTitle(evt.target.value);
};
const handleKeyDown = (evt) => {
if (evt.key === "Enter") onSubmit(title);
};
const inputElem = useRef(null);
useEffect(() => {
inputElem.current.focus();
});
return (
<li className="kanban-card">
<h4>添加新卡片</h4>
<div className="card-title">
<input
ref={inputElem}
type="text"
value={title}
onChange={handleChange}
onKeyDown={handleKeyDown}
></input>
</div>
</li>
);
};
const DATE_STORE_KEY = "kanban_data_store";
const COLUMN_KEY_TODO = "todo";
const COLUMN_KEY_ONGONING = "ongoing";
const COLUMN_KEY_DONE = "done";
function App() {
const [todoList, setTodoList] = useState([
{ title: "開發(fā)任務(wù)-1", status: "2022-05-22 18:15" },
]);
const [ongoingList, setOngoingList] = useState([
{ title: "進(jìn)行任務(wù)-1", status: "2022-08-22 18:15" },
]);
const [doneList, setDoneList] = useState([
{ title: "完成任務(wù)-1", status: "2022-10-22 18:15" },
]);
const [showAdd, setShowAdd] = useState(false);
const handleAdd = (evt) => {
setShowAdd(true);
};
const handleSubmit = (title) => {
// todoList.unshift({title,status:new Date().toDateString()});
setTodoList((current) => [{ title, status: new Date() + " " }, ...current]);
setShowAdd(false);
};
const handleSaveAll = () => {
const data = JSON.stringify({
todoList,
ongoingList,
doneList,
});
window.localStorage.setItem(DATE_STORE_KEY, data);
};
useEffect(() => {
const data = window.localStorage.getItem(DATE_STORE_KEY);
setTimeout(() => {
if (data) {
const kanbanColumnData = JSON.parse(data);
setTodoList(kanbanColumnData.todoList);
}
}, 1000);
});
const [draggedItem, setDraggedItem] = useState(null);
const [dragSource, setDragSource] = useState(null);
const [dragTarget, setDragTarget] = useState(null);
const handleDrop = (evt) => {
if (!draggedItem || !dragSource || !dragTarget || dragSource === dragTarget) { return; }
const updaters = {
[COLUMN_KEY_TODO]: setTodoList,
[COLUMN_KEY_ONGONING]: setOngoingList,
[COLUMN_KEY_DONE]: setDoneList
};
if (dragSource) {
updaters[dragSource]((currentStat) => {
return currentStat.filter((item) => !Object.is(item, draggedItem));
});
}
if (dragTarget) {
updaters[dragTarget]((currentStat) => {
if (currentStat.length > 0) {
return [draggedItem, ...currentStat]
} else {
return [draggedItem]
}
})
}
};
return (
<div className="App">
<header className="App-header">
<h1>
我的看板<button onClick={handleSaveAll}>保存所有卡片</button>{" "}
</h1>
</header>
<KanbanBoard>
<KanbanColumn
className="column-todo"
title={
<>
待處理
<button disabled={showAdd} onClick={handleAdd}>
⊕添加新卡片
</button>{" "}
</>
}
setIsDragSource={(isSrc) =>
setDragSource(isSrc ? COLUMN_KEY_TODO : null)
}
setIsDragTarget={(isTarget) =>
setDragTarget(isTarget ? COLUMN_KEY_TODO : null)
}
onDrop={handleDrop}
>
{/* <h2>
待處理
<button disabled={showAdd} onClick={handleAdd}>
⊕添加新卡片
</button>{" "}
</h2> */}
{/* <ul> */}
{showAdd && <AddKanbanCard onSubmit={handleSubmit} />}
{todoList && todoList.map((item) => (
<KanbanCard
{...item}
key={item.title}
onDragStart={() => setDraggedItem(item)}
/>
))}
{/* </ul> */}
</KanbanColumn>
<KanbanColumn className="column-ongoing" title={"進(jìn)行中"} setIsDragSource={(isSrc) =>
setDragSource(isSrc ? COLUMN_KEY_ONGONING : null)
}
setIsDragTarget={(isTarget) =>
setDragTarget(isTarget ? COLUMN_KEY_ONGONING : null)
}
onDrop={handleDrop}>
{/* <h2>進(jìn)行中</h2>
<ul> */}
{ongoingList && ongoingList.map((item) => (
<KanbanCard
{...item}
key={item.title}
onDragStart={() => setDraggedItem(item)}
/>
))}
{/* </ul> */}
</KanbanColumn>
<KanbanColumn className="column-done" title={"已處理"} setIsDragSource={(isSrc) =>
setDragSource(isSrc ? COLUMN_KEY_DONE : null)
}
setIsDragTarget={(isTarget) =>
setDragTarget(isTarget ? COLUMN_KEY_DONE : null)
}
onDrop={handleDrop}>
{/* <h2>已處理</h2>
<ul> */}
{doneList && doneList.map((item) => (
<KanbanCard
{...item}
key={item.title}
onDragStart={() => setDraggedItem(item)}
/>
))}
{/* </ul> */}
</KanbanColumn>
</KanbanBoard>
</div>
);
}
export default App;這時(shí)拖拽基本完成,此時(shí)有一個(gè)bug,就是待處理添加新卡片的時(shí)候,拖拽之后的數(shù)據(jù)出現(xiàn)混亂??!如下所示:

首先問題定位,移動(dòng)的來源項(xiàng)出現(xiàn)了問題,看代碼之后發(fā)現(xiàn)拖拽處理來源項(xiàng)沒有問題,那一定是那塊調(diào)用更新todaList出現(xiàn)了問題,問題定位在useEffect,使用時(shí)如果useEffect的第二個(gè)參數(shù)不傳就在組件所有更新都執(zhí)行(即任何時(shí)候),傳個(gè)空數(shù)組僅在掛載和卸載的時(shí)候執(zhí)行,或者傳個(gè)你想要去進(jìn)行更新時(shí)候去執(zhí)行(默認(rèn)情況下,effect 將在每輪渲染結(jié)束后執(zhí)行,但你可以選擇讓它 在只有某些值改變的時(shí)候 才執(zhí)行。)
更改代碼如下:
useEffect(() => {
const data = window.localStorage.getItem(DATE_STORE_KEY);
setTimeout(() => {
if (data) {
const kanbanColumnData = JSON.parse(data);
setTodoList(kanbanColumnData.todoList);
}
}, 1000);
},[]);
useEffect新增空數(shù)組效果展示:

學(xué)習(xí)一個(gè)新的框架總是會(huì)進(jìn)行對(duì)比,
一:React的單項(xiàng)數(shù)據(jù)流和Vue中的雙向綁定有什么區(qū)別?
在我看了Vue 雙向綁定其實(shí)是語法糖罷了,其原理其實(shí)是Object.defineProperty()對(duì)數(shù)據(jù)進(jìn)行劫持,監(jiān)聽到變化就去對(duì)數(shù)據(jù)進(jìn)行更改;
而React 中的單項(xiàng)數(shù)據(jù)流做到了對(duì)原有數(shù)據(jù)的保護(hù),你不能去直接去對(duì)Props進(jìn)行更改,而是在需要賦值給State,然后再SetState中去進(jìn)行更改,當(dāng)然Hooks中提供了useState方法,使得開發(fā)者更加方便的去對(duì)數(shù)據(jù)進(jìn)行處理和更改(我可太喜歡Hooks了很省事?。。?/p>
二:JSX 是什么?
學(xué)習(xí)React的時(shí)候,寫組件的時(shí)候?qū)戫撁嬖厥怯肑SX來寫的,即Render里面是用JSX來實(shí)現(xiàn)的,渲染之后其實(shí)質(zhì)是React.createElement,他只是語法糖 實(shí)現(xiàn)React組件的一部分而已,對(duì)比Vue中Template,(我更喜歡Vue的實(shí)現(xiàn),更符合開發(fā)者)不過Vue也可用JSX來實(shí)現(xiàn);
三:函數(shù)式組件(Hooks)與類組件(Class)優(yōu)缺點(diǎn)?
宋一瑋老師的數(shù)據(jù)表明函數(shù)式比類組件使用更多,并且函數(shù)組件基本上涵蓋了類組件的功能點(diǎn),除了(只有類組件才能成為錯(cuò)誤邊界)
從React官網(wǎng)中開局也是用類組件來領(lǐng)進(jìn)門的,我是看完了官網(wǎng)的基礎(chǔ)才來學(xué)習(xí)課程的才覺得學(xué)習(xí)沒有那么吃力反而能加深理解;(老師的反其道而行可能不太適合初學(xué)者)
四:CSS可否想JS一樣應(yīng)用在組件中?
可以的,使用emotion來應(yīng)用到JSX中(主要代碼中有使用),不過CSS中傳入JS數(shù)據(jù)確實(shí)很方便但是在運(yùn)行emotion時(shí)會(huì)創(chuàng)建大量的<style>標(biāo)簽,有可能影響頁面性能。
CSS-in-JS 技術(shù)能幫我們做到樣式隔離、提升組件樣式的可維護(hù)性、可復(fù)用性。
五:常用的hooks——useState,useEffect、useRef
到此這篇關(guān)于React實(shí)現(xiàn)卡片拖拽效果流程詳解的文章就介紹到這了,更多相關(guān)React卡片拖拽內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React?component.forceUpdate()強(qiáng)制重新渲染方式
這篇文章主要介紹了React?component.forceUpdate()強(qiáng)制重新渲染方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10
react-native 封裝視頻播放器react-native-video的使用
本文主要介紹了react-native 封裝視頻播放器react-native-video的使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01
react-draggable實(shí)現(xiàn)拖拽功能實(shí)例詳解
這篇文章主要給大家介紹了關(guān)于react-draggable實(shí)現(xiàn)拖拽功能的相關(guān)資料,React-Draggable一個(gè)使元素可拖動(dòng)的簡單組件,文中通過代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2023-08-08
React 項(xiàng)目遷移 Webpack Babel7的實(shí)現(xiàn)
這篇文章主要介紹了React 項(xiàng)目遷移 Webpack Babel7的實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-09-09
React錯(cuò)誤邊界Error Boundaries詳解
錯(cuò)誤邊界是一種React組件,這種組件可以捕獲發(fā)生在其子組件樹任何位置的JavaScript錯(cuò)誤,并打印這些錯(cuò)誤,同時(shí)展示降級(jí)UI,而并不會(huì)渲染那些發(fā)生崩潰的子組件樹2022-12-12

