Project 5

Project 5
Kanban Board with Drag-and-Drop
Cards that move between columns by dragging, backed by a more complex piece of nested state
Difficulty: Advanced Introduces: native HTML5 drag-and-drop

Every project so far has worked with a flat array — a list of todos, a list of notes. A Kanban board's state is one level more complex: an object where each key holds its own array (one per column), and the core interaction — dragging a card from one column to another — means immutably removing an item from one array and adding it to a different one, in the same update. This project also introduces the browser's native drag-and-drop events, which React doesn't wrap in anything special — they're used directly, the same way onClick always has been.

Skills This Project Exercises
Nested state shape Immutable array updates Native drag-and-drop events Conditional styling (drag-over) Lists + keys
The native drag-and-drop events you'll need
No library is required for the core requirement — five HTML attributes/events cover it: draggable on the card itself; onDragStart (fires when dragging begins — store which card and which column it came from); onDragOver on each column (must call e.preventDefault(), or the column will never accept a drop); and onDrop on each column (where the actual state update happens, moving the card into that column).

Requirements

  • At least three columns (e.g. "To Do," "In Progress," "Done"), each showing its own list of cards.
  • A way to add a new card with some text, landing in a specific column.
  • Dragging a card from one column and dropping it on another moves it there — removed from the original column's list, added to the new one.
  • A card can be deleted from whichever column it's currently in.
  • The column currently being dragged over should look visually different (e.g. a highlighted border) while a card is hovering over it.
  • Dropping a card back into its original column should not duplicate it or lose it.

Suggested Component Breakdown

App └── KanbanBoard // holds { todo: [...], inProgress: [...], done: [...] } ├── AddCardForm // text input + column choice, adds to that column's array └── Column (×3) // onDragOver / onDrop live here, plus drag-over highlight state └── Card (×N) // draggable={true}, onDragStart, delete button

A reasonable shape for the state itself: { todo: [{ id, text }, ...], inProgress: [...], done: [...] }. KanbanBoard owns this whole object — exactly the "lowest common ancestor" reasoning from Fundamentals Chapter 10, since moving a card between columns is fundamentally a change that affects two columns (siblings) at once, which only their shared parent can coordinate.

A Reasonable Build Order

  1. Hardcode the columns object with a few sample cards in each, and get all three columns rendering their cards correctly with .map() before touching drag-and-drop at all.
  2. Move the hardcoded object into useState, and build AddCardForm so new cards can be added to a chosen column's array.
  3. Add draggable and onDragStart to Card, storing the card's id and its current column name (state, a ref, or e.dataTransfer all work) somewhere onDrop can read it from later.
  4. Add onDragOver (with e.preventDefault()) and onDrop to Column. On drop, build a new columns object: filter the card out of its source column's array, and add it to the destination column's array.
  5. Add the drag-over highlight — a small piece of state in each Column (or lifted up, tracking which column id is currently being dragged over) toggled by onDragEnter/onDragLeave.
  6. Add the delete button per card last, once the drag-and-drop flow is solid.
Two classic gotchas
Forgetting e.preventDefault() inside onDragOver is the single most common reason a drop silently does nothing — the browser's default behavior for drag-over is to reject the drop entirely unless that default is explicitly cancelled. Separately, building the new columns object by mutating the existing arrays in place (e.g. .push() or .splice() directly on a state array) won't reliably trigger a re-render — always build new arrays (.filter(), spread) and a new outer object, the same immutability rule from Fundamentals Chapter 3, just one level deeper this time.
Stretch Goals
  • Support reordering cards within the same column, not just moving between columns.
  • Persist the whole board to localStorage, same pattern as Project 3's notes.
  • Swap the native drag-and-drop API for a dedicated library (e.g. @dnd-kit/core), which adds proper keyboard/accessibility support that the native API lacks.
  • Let columns themselves be added, renamed, or removed, rather than being fixed at three.
  • Add a card-detail view (click to expand a card into a longer description, due date, etc.).
This project has no single solution file — the requirements above and your own judgment are the spec. Compare notes against the suggested component breakdown once you have something working, not before.