dnd.mp4
A kanban board implementation with drag and drop fully integrated with a real API and database. Built to show how to implement @hello-pangea/dnd with server state, optimistic UI, and high render performance in a production-grade setup.
| Layer | Technology |
|---|---|
| Framework | Next.js 15 (App Router) |
| UI | React 19 |
| Styling | Tailwind CSS v4 |
| Drag & Drop | @hello-pangea/dnd |
| Server State | TanStack Query v5 |
| Client State | Jotai |
| Database | SQLite (better-sqlite3) |
| API | Next.js Route Handlers |
| Validation | Zod |
Drag and drop connected to actual HTTP endpoints — not mock data or local state. Every card move triggers a PATCH /api/issues/:id/position with the new position and status, persisted in a SQLite database.
GET /api/issues
POST /api/issues
GET /api/issues/:id
PATCH /api/issues/:id
DELETE /api/issues/:id
PATCH /api/issues/:id/position
The board updates instantly on drop — before the API responds. TanStack Query handles the optimistic state: the UI reflects the new card position immediately, and rolls back automatically if the request fails.
1. User drops card
2. UI updates instantly (optimistic)
3. PATCH request fires in background
4. On success: server state is confirmed
5. On error: UI rolls back to previous state
Cards are sorted by a position field (float). Moving a card calculates a new position between its neighbors — no re-indexing of the full list needed.
Before: [pos=1] [pos=2] [pos=3] [pos=4]
Move last card between 1 and 2:
After: [pos=1] [pos=1.5] [pos=2] [pos=3]
Only 1 database row is updated per drop, regardless of how many cards are in the column.
@hello-pangea/dnd (a fork of react-beautiful-dnd) forces re-renders on all children of Droppable and Draggable during any drag. This project uses React.memo with a custom comparator to block those forced re-renders:
export const DragAndDropReRenderBreaker = memo(
({ children }: React.PropsWithChildren<{ memoizationId: string }>) => {
return <>{children}</>;
},
(prev, next) => prev.memoizationId === next.memoizationId,
);Applied at two levels — column and card — so dragging one card doesn't re-render any other card or column content.
bun install
bun devThe database is created and seeded automatically on first run.
src/
├── app/
│ ├── layout.tsx # Root HTML layout, fonts, metadata
│ ├── page.tsx # Home route — renders KanbanApp
│ └── api/
│ └── issues/
│ ├── route.ts # GET /api/issues, POST /api/issues
│ └── [id]/
│ ├── route.ts # GET, PATCH, DELETE /api/issues/:id
│ └── position/
│ └── route.ts # PATCH /api/issues/:id/position
│
├── db/
│ ├── index.ts # SQLite connection singleton (WAL mode)
│ ├── schema.ts # CREATE TABLE issues
│ ├── queries.ts # getAllIssues, getIssueById, createIssue,
│ │ # updateIssue, deleteIssue, updateIssuePosition
│ ├── seed.ts # Populates the database on first run
│ └── setup.ts # Runs schema + seed before any request
│
├── features/
│ └── issues/
│ ├── components/
│ │ ├── kanban-app.tsx # Providers: QueryClient, Jotai, Toaster
│ │ ├── board/
│ │ │ ├── kanban-board.tsx # Renders all columns side by side
│ │ │ └── kanban-board-drag-drop-context.tsx # DragDropContext, onDragEnd handler,
│ │ │ # fractional position calc, API sync
│ │ ├── column/
│ │ │ ├── kanban-column.tsx # Droppable wrapper + memo optimization
│ │ │ ├── kanban-column-header.tsx # Title, status dot, card count
│ │ │ └── kanban-column-cards-container.tsx # Scrollable droppable card list
│ │ └── card/
│ │ ├── kanban-card.tsx # Title, tags, priority, date, assignee avatar
│ │ ├── kanban-card-draggable-container.tsx # Draggable wrapper + drop animation
│ │ └── kanban-card-priority-icon.tsx # SVG bars (urgent/high/medium/low/none)
│ │
│ ├── hooks/
│ │ ├── use-issues.ts # useQuery → GET /api/issues + Zod validation
│ │ └── use-update-issue-status.ts # useMutation → PATCH with optimistic update + toast
│ │
│ ├── schemas/
│ │ ├── issue.types.ts # ColumnStatus, Priority, KanbanCard, KanbanTag
│ │ ├── issue.schemas.ts # Zod schemas for issue and list validation
│ │ └── columns.ts # Column definitions (backlog → done)
│ │
│ └── data/
│ └── mock-data.ts # 25 sample cards for initial seeding
│
├── shared/
│ ├── components/
│ │ ├── avatar.tsx # Initials avatar with custom background color
│ │ ├── tag-badge.tsx # Colored badge for issue tags
│ │ └── drag-and-drop-re-render-breaker.tsx # React.memo wrapper blocking dnd re-renders
│ └── utils/
│ └── compute-new-position.ts # Fractional indexing: calculates position between neighbors
│
└── index.css # Tailwind setup, scrollbar style, body defaults