|
| 1 | +When building **Tier Forge**, I needed a flexible way to move items between the "pool" and various "tiers". While libraries like `react-beautiful-dnd` or `dnd-kit` are excellent, sometimes you just want full control without the overhead. |
| 2 | + |
| 3 | +Here is how I implemented a robust drag-and-drop system using only the native HTML5 API and React state. |
| 4 | + |
| 5 | +## The State Architecture |
| 6 | + |
| 7 | +The key to a good DnD system is centralized state. In `TierForge`, the state is held in the parent component: |
| 8 | + |
| 9 | +```javascript |
| 10 | +const [tiers, setTiers] = useState(DEFAULT_TIERS); // The board |
| 11 | +const [poolItems, setPoolItems] = useState([]); // The unranked items |
| 12 | +const [dragData, setDragData] = useState(null); // What are we dragging? |
| 13 | +``` |
| 14 | + |
| 15 | +We track `dragData` to know *what* is moving (`itemId`) and *where* it came from (`sourceId`). |
| 16 | + |
| 17 | +## The Handlers |
| 18 | + |
| 19 | +We need three main handlers: `onDragStart`, `onDragOver`, and `onDrop`. |
| 20 | + |
| 21 | +### 1. Starting the Drag |
| 22 | + |
| 23 | +When a user grabs an item, we store its ID and source container ID. We also set `dataTransfer` for compatibility. |
| 24 | + |
| 25 | +```javascript |
| 26 | +const handleDragStart = (e, itemId, sourceId) => { |
| 27 | + setDragData({ itemId, sourceId }); |
| 28 | + e.dataTransfer.effectAllowed = 'move'; |
| 29 | + // Fallback for some browsers |
| 30 | + e.dataTransfer.setData('text/plain', JSON.stringify({ itemId, sourceId })); |
| 31 | +}; |
| 32 | +``` |
| 33 | + |
| 34 | +### 2. Allowing the Drop |
| 35 | + |
| 36 | +By default, HTML elements don't accept drops. We must prevent the default behavior. |
| 37 | + |
| 38 | +```javascript |
| 39 | +const handleDragOver = (e) => { |
| 40 | + e.preventDefault(); |
| 41 | + e.dataTransfer.dropEffect = 'move'; |
| 42 | +}; |
| 43 | +``` |
| 44 | + |
| 45 | +### 3. Handling the Drop |
| 46 | + |
| 47 | +This is where the magic happens. When an item is dropped, we: |
| 48 | +1. Identify the **Source** (where it came from) and **Target** (where it landed). |
| 49 | +2. If Source === Target, do nothing (or reorder). |
| 50 | +3. Find the item in the Source array. |
| 51 | +4. Remove it from the Source. |
| 52 | +5. Add it to the Target. |
| 53 | + |
| 54 | +```javascript |
| 55 | +const handleDrop = (e, targetId) => { |
| 56 | + e.preventDefault(); |
| 57 | + const data = dragData || JSON.parse(e.dataTransfer.getData('text/plain')); |
| 58 | + if (!data) return; |
| 59 | + |
| 60 | + const { itemId, sourceId } = data; |
| 61 | + if (sourceId === targetId) return; |
| 62 | + |
| 63 | + // ... Logic to find item, remove from source, add to target ... |
| 64 | + // This involves setTiers() and setPoolItems() updates. |
| 65 | +}; |
| 66 | +``` |
| 67 | + |
| 68 | +## The Components |
| 69 | + |
| 70 | +### Draggable Item |
| 71 | +The item itself needs the `draggable` attribute and the start handler. |
| 72 | + |
| 73 | +```jsx |
| 74 | +<div |
| 75 | + draggable |
| 76 | + onDragStart={(e) => onDragStart(e, item.id, sourceId)} |
| 77 | + className="cursor-grab active:cursor-grabbing ..." |
| 78 | +> |
| 79 | + {/* Content */} |
| 80 | +</div> |
| 81 | +``` |
| 82 | + |
| 83 | +### Drop Zone |
| 84 | +The container (Tier or Pool) listens for drag-over and drop events. |
| 85 | + |
| 86 | +```jsx |
| 87 | +<div |
| 88 | + onDragOver={handleDragOver} |
| 89 | + onDrop={(e) => handleDrop(e, containerId)} |
| 90 | + className="..." |
| 91 | +> |
| 92 | + {/* Render Items */} |
| 93 | +</div> |
| 94 | +``` |
| 95 | + |
| 96 | +## Why Native API? |
| 97 | + |
| 98 | +1. **Zero Dependencies:** Keeps the bundle size small. |
| 99 | +2. **Full Control:** I can define exactly how state updates happen. |
| 100 | +3. **Performance:** Direct DOM events are highly performant. |
| 101 | + |
| 102 | +This pattern powers the entire Tier Forge experience, allowing smooth transitions of assets between the chaotic pool and the structured tiers. |
0 commit comments