DEV Community

Cover image for I deleted the drag-and-drop library and shipped four buttons instead
Ibukun Demehin
Ibukun Demehin

Posted on

I deleted the drag-and-drop library and shipped four buttons instead

My shopping app shipped item reordering last week, and it contains zero drag gestures. You tap a ⇅ button on a row and pick from four menu options: Move to top · Move up · Move down · Move to bottom. That's it. This is the story of the much fancier version I built the same afternoon — and deleted before dinner.

The library never even made it into a commit. That's the tell.

TL;DR — Drag-to-reorder on a grouped SectionList (inside a gesture-handler root, with bottom sheets, pull-to-refresh, and swipe actions already competing for touches) triggered a cascade of Android issues: a crash, a frozen refresh, overlapping headers, janky scroll. Each fix surfaced the next — the signature of an integration fight, not a bug. The requirement said reorder, not drag — so I deleted the library and shipped a plain SectionList + a bottom-sheet menu that renumbers position optimistically. Boring won.

(Part 8 of Building CannyCart, a voice-first shopping app I'm building in public. Self-contained — no earlier context needed.)

The setup: a list that was already gesture-crowded

The Shopping tab is a grouped checklist — items under aisle headers (Produce, Dairy, …) in a React Native SectionList. By the time reordering came up, that screen was already juggling touch handlers from every direction:

  • the app-wide GestureHandlerRootView (load-bearing, outermost in the provider stack),
  • gorhom bottom sheets with pan-to-dismiss,
  • pull-to-refresh on the list,
  • checkbox taps, row taps (edit sheet), row swipe-to-delete.

"Users should reorder items" was on the list, and drag-and-drop is the obviously right interaction for that — every design instinct says so. So I reached for a drag library, react-native-reorderable-list, and wired it in place of the plain list.

The cascade

What followed, in order (from my build notes of that session — this all happened inside one afternoon):

  1. A crash. The reorderable list didn't want to live inside the existing gesture/layout arrangement.
  2. Restructure, retry — the crash cleared, and pull-to-refresh froze. The refresh spinner and the drag recognizer were now fighting over the same vertical pan.
  3. Work around that — section headers started overlapping rows after a drag; the library's layout animation and the sticky-header math disagreed about where things were.
  4. And underneath it all, scroll got janky — the one thing a list must never be.

Each fix surfaced the next problem, one layer down. That pattern is worth naming, because it's the diagnostic: when fixes reveal rather than resolve, you're not debugging a bug — you're fighting an integration. The library was fine. My screen was fine. They just couldn't share a gesture system on Android without me re-architecting one of them.

Every one of these, on its own, felt fixable. I know how these bugs go — another hour, another workaround. That's precisely the trap.

The decision point

Somewhere around the header overlap, I stopped and re-read the actual requirement. It wasn't "users can drag items." It was "users can reorder items."

Drag is one implementation of reorder — the prettiest one, and on this particular screen, the most expensive one. A menu is another implementation: tap ⇅ on the row, pick Move up / Move to top / etc. It has zero gesture surface — a tap can't fight a pan recognizer, can't freeze a refresh spinner, can't confuse a sticky header. And a menu is honestly more usable in context: a grocery list is operated one-handed, walking, pushing a trolley. Long-press-and-drag with your thumb while the phone wobbles is a worse interaction than two taps, and it's kinder to anyone without fine motor control.

So: npm uninstall. The library left no trace — not in a commit, not in the lockfile. The whole arc, from install to delete, fits inside one working session. The best review the drag version ever got is that it never survived to a commit.

What shipped: four buttons and some renumbering

The row's ⇅ button opens a bottom sheet. The options self-limit — an item at the top of its aisle doesn't offer "Move up":

// reorder-sheet.tsx (excerpt)
export type MoveAction = "top" | "up" | "down" | "bottom";

{canUp ? <MoveRow icon={} label="Move to top" onPress={() => onMove("top")} /> : null}
{canUp ? <MoveRow icon={} label="Move up" onPress={() => onMove("up")} /> : null}
{canDown ? <MoveRow icon={} label="Move down" onPress={() => onMove("down")} /> : null}
{canDown ? <MoveRow icon={} label="Move to bottom" onPress={() => onMove("bottom")} /> : null}
Enter fullscreen mode Exit fullscreen mode

The move itself is array surgery plus a renumber. Items live under aisle sections but position is global across the list, so after moving within the category I re-derive positions for everything and only write the rows whose number actually changed:

// shopping/index.tsx (excerpt)
function applyReorder(catKey: string, from: number, to: number) {
  const catItems = [...section.data];
  const [moved] = catItems.splice(from, 1);
  catItems.splice(dest, 0, moved);

  // Rebuild the global order, then diff: only changed positions get written.
  const updates: { id: string; position: number }[] = [];
  ordered.forEach((it, i) => {
    if (it.position !== i) updates.push({ id: it.id, position: i });
  });

  // Optimistic: patch + resort the cache, then persist.
  qc.setQueryData(["shoppingItems", activeList.id], /* patch + sort by position */);
  reorderItems.mutate({ listId: activeList.id, updates });
}
Enter fullscreen mode Exit fullscreen mode

The cache patch means the row hops instantly; the mutation persists in the background (and, since Part 7, even offline). Total surface area: one sheet component, one function, no new dependencies.

The shopping list: each row carries a ⇅ reorder button on the right — a tap target, not a gesture

The reorder bottom sheet: the item's name,

The villain found in the wreckage

The debugging detour did pay one real dividend. While untangling gesture handlers I found this, wrapped around the entire screen:

- <TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
    <View className="flex-1">
      {/* …the whole shopping screen… */}
    </View>
- </TouchableWithoutFeedback>
Enter fullscreen mode Exit fullscreen mode

It was there for a reasonable purpose — tap anywhere to dismiss the keyboard. But on Android it was eating the first touch of every scroll: touch the list while the wrapper is watching, and your gesture goes to the touchable first, so the scroll doesn't start until the second attempt. That subtle "the list feels slightly dead" wrongness had been there the whole time — the drag investigation is what finally surfaced it. It's gone now (keyboard dismissal moved to the specific interactions that need it), and scroll response was noticeably better even before any reorder feature existed.

Wrong theories sometimes pay rent.

What I took away

  • Re-read the requirement before debugging the solution. "Reorder" ≠ "drag." I was an hour into fixing an implementation nobody had actually asked for.
  • When fixes reveal instead of resolve, stop. A bug yields to a fix. An integration fight yields a new bug per fix. Different problem, different move — change the approach, not the patch.
  • On Android, every gesture library negotiates with every other one. Root view, sheets, refresh, swipes, drag — each is reasonable alone; the combination is the risk. Budget for the interactions, not the features.
  • A deleted dependency is the cheapest dependency. Zero commits, zero lockfile entries, zero future upgrades that break. The boring version has needed no attention since.

Next up

The aisle headers those items sort under have their own story: the categories are free-text labels an LLM stamps on each item — so how do you let a user reorder, rename, and emoji things that aren't really entities? That's Post 9.

What's the library you deleted and immediately felt relief? I'm collecting evidence that "rip it out" is an underrated debugging technique.

Top comments (0)