fc-react-dndnpm ↗
Example

Sortable list

The base case. useSortable makes each row draggable, applySortEnd resolves the new order, and the whole style object from the hook carries the transform, the settle transition, and touch-action.

Reorder a list whose state holds objects

State usually holds objects, not bare ids — so the ids are derived once with useMemo and passed to SortableList, which keeps its projection memo stable. applySortEnd runs inside the state updater and resolves the landing slot by neighbour id, so a concurrent edit can never drop a row at a stale index. Rows are keyed by id, because every drag reorders the list.
To pick up a draggable item, press Space or Enter. While dragging, use the arrow keys to move the item. Press Space or Enter again to drop, or press Escape to cancel.
src/examples/sortable-list-example.tsx
'use client'

import type { SortEndEvent } from 'fc-react-dnd'
import { applySortEnd, DndProvider, SortableList, useSortable } from 'fc-react-dnd'
import { useCallback, useMemo, useState } from 'react'
import { listStyle, rowStyle } from './_shared/example-styles'

type Task = { id: string; title: string }

const INITIAL_TASKS: readonly Task[] = [
  { id: 'write', title: 'Write the post' },
  { id: 'review', title: 'Review it' },
  { id: 'illustrate', title: 'Add the diagrams' },
  { id: 'ship', title: 'Ship it' },
]

const Row = ({ task }: { task: Task }) => {
  const { setNodeRef, handleProps, isDragging, isOver, style } = useSortable({ id: task.id })

  return (
    <li>
      <button
        type="button"
        ref={setNodeRef}
        {...handleProps}
        // `style` carries transform, the settle transition, and touch-action. Everything after
        // the spread is this example's own look.
        style={{
          ...rowStyle,
          ...style,
          opacity: isDragging ? 0.4 : 1,
          borderColor: isOver ? '#2563eb' : '#e2e8f0',
        }}
      >
        <span aria-hidden="true">⠿</span>
        {task.title}
      </button>
    </li>
  )
}

export const SortableListExample = () => {
  const [tasks, setTasks] = useState<readonly Task[]>(INITIAL_TASKS)

  // Derived once, and rebuilt only when `tasks` is replaced — the referential stability
  // `SortableList` needs to key its projection memo. Inlining `tasks.map(...)` here is the one
  // mistake that recomputes the projection once per row per pointermove.
  const taskIds = useMemo(() => tasks.map((task) => task.id), [tasks])

  const handleSortEnd = useCallback((event: SortEndEvent) => {
    // Applied inside the updater, so the landing slot resolves against the queued state — not a
    // snapshot from when the drop happened.
    setTasks((current) => applySortEnd(current, event, (task) => task.id))
  }, [])

  return (
    <DndProvider>
      <SortableList items={taskIds} onSortEnd={handleSortEnd}>
        <ul style={listStyle}>
          {tasks.map((task) => (
            <Row key={task.id} task={task} />
          ))}
        </ul>
      </SortableList>
    </DndProvider>
  )
}