AI Will Happily Help You Build the Wrong Abstraction

AI Will Happily Help You Build the Wrong Abstraction

What replacing dnd-kit with React Grid Layout taught me about library fit, data models, and engineering judgment in AI-assisted development.

I was building a global dashboard made of draggable cards. Some cards were small stats. Others contained tables or larger, top-down views. The cards needed to move, span different numbers of columns, resize, compact around one another, respond to the viewport, and remember each user’s layout.

I started with dnd-kit.

That decision made sense at a glance. The feature involved dragging and dropping React components, and dnd-kit is an excellent drag-and-drop toolkit. We already used it elsewhere in the application. AI knew the library, the code compiled, and the first version looked close enough to keep going.

So we kept going.

Whenever the interaction felt wrong, I asked AI to fix it. When a card jumped unexpectedly, we adjusted collision detection. When the dragged card looked awkward, we added a custom overlay. When cards of different sizes did not settle naturally, we changed CSS Grid behavior and sorting logic. Each prompt produced another plausible local improvement.

The implementation became increasingly complicated without becoming fundamentally correct.

The problem was not that AI could not write the code. The problem was that I had asked it to keep improving an answer before I had challenged the question.

The First Model: A Dashboard as a Sortable Array

The original dashboard treated its cards as an ordered collection. At the center of the implementation was the familiar dnd-kit sortable pattern:

<DndContext
  collisionDetection={closestCenter}
  onDragStart={handleDragStart}
  onDragEnd={handleDragEnd}
>
  <SortableContext
    items={tiles.map((tile) => tile.id)}
    strategy={rectSortingStrategy}
  >
    <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
      {tiles.map((tile) => (
        <DashboardTile key={tile.id} tile={tile} />
      ))}
    </div>
  </SortableContext>

  <DragOverlay>{/* custom drag preview */}</DragOverlay>
</DndContext>

Each tile used useSortable to connect itself to that context:

const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
  useSortable({ id: tile.id });

const style = {
  transform: CSS.Transform.toString(transform),
  transition: isDragging ? undefined : transition,
  opacity: isDragging ? 0 : 1,
};

When a drag ended, we found the old and new indexes and reordered the array:

function handleDragEnd({ active, over }: DragEndEvent) {
  if (!over || active.id === over.id) return;

  const oldIndex = tiles.findIndex((tile) => tile.id === active.id);
  const newIndex = tiles.findIndex((tile) => tile.id === over.id);

  const next = [...tiles];
  const [moved] = next.splice(oldIndex, 1);
  next.splice(newIndex, 0, moved);

  setTiles(next);
}

That is good code for a sortable collection. It answers a clear question: after dragging, what is the new order of these items?

It did not answer the question our dashboard was actually asking.

Order Is Not Geometry

A sortable array has a small state model:

type TileOrder = Array<{
  id: string;
  position: number;
}>;

Our dashboard needed a spatial state model:

type TileLayout = Array<{
  i: string;
  x: number;
  y: number;
  w: number;
  h: number;
  minW?: number;
  maxW?: number;
  minH?: number;
  maxH?: number;
}>;

That difference is the entire story.

The dnd-kit sortable preset supports lists and grids, but its job is sorting. Its strategies calculate how sortable items should move relative to an ordered set. Its collision algorithms answer questions such as “which droppable is closest to the dragged item?” They do not decide how a four-column dashboard should repack several differently sized rectangles after one of them becomes wider.

CSS Grid could make our ordered array look like a dashboard. grid-auto-flow: dense could even fill some visual gaps. But the browser’s placement was not application state. We could not reliably serialize it, restore it, apply per-tile size constraints, or use it to resolve collisions after resizing.

This is where the code was heading toward a homegrown layout engine. To make the original abstraction fit, we would have needed logic resembling this:

function placeTile(tile, occupiedCells) {
  // Find a rectangle large enough for tile.w x tile.h.
  // Check every occupied cell for collisions.
  // Move displaced tiles out of the way.
  // Compact the remaining gaps.
  // Repeat until the layout is stable.
}

function resizeTile(tile, nextWidth, layout) {
  // Clamp the width.
  // Keep the tile inside the column boundary.
  // Detect every new overlap.
  // Reposition neighbors.
  // Compact again.
}

None of that is impossible to build. It is also no longer “adding drag and drop.” It is implementing the hard part of a two-dimensional layout system.

The code felt hacky because the data model and the product model disagreed. Every new behavior had to be translated through position, even though the feature cared about x, y, w, and h.

The Question That Changed the Implementation

When I came back to the dashboard later, I finally stopped asking, “How do I make dnd-kit do this?”

I asked, “Is dnd-kit the right library for this?”

That led me to React Grid Layout (RGL), whose own description is almost a direct restatement of the feature: a draggable and resizable grid layout with responsive breakpoints for React.

More importantly, RGL’s core abstraction matched our domain. A layout item already had coordinates and dimensions:

const layout = [
  { i: "open-tasks", x: 0, y: 0, w: 1, h: 1 },
  { i: "recent-work", x: 1, y: 0, w: 2, h: 4, minW: 2 },
  { i: "portfolio", x: 0, y: 4, w: 4, h: 4, minW: 2 },
];

Instead of constructing dashboard behavior from sortable primitives, we could describe the behavior directly:

const { width, containerRef, mounted } = useContainerWidth();

return (
  <div ref={containerRef}>
    {mounted && (
      <Responsive
        width={width}
        layouts={{
          lg: layout,
          mobile: createMobileLayout(layout),
        }}
        breakpoints={{ lg: 1024, mobile: 0 }}
        cols={{ lg: 4, mobile: 1 }}
        rowHeight={110}
        compactor={verticalCompactor}
        dragConfig={{
          enabled: editing,
          bounded: true,
          handle: ".rgl-drag-handle",
          cancel: ".rgl-interactive",
        }}
        resizeConfig={{
          enabled: editing,
          handles: ["e", "w"],
        }}
        onLayoutChange={handleLayoutChange}
      >
        {tiles.map((tile) => (
          <div key={tile.id}>
            <DashboardTile tile={tile} />
          </div>
        ))}
      </Responsive>
    )}
  </div>
);

The API reads like the requirements:

  • Four columns on desktop and one on mobile
  • Fixed row units
  • Vertical compaction
  • Bounded dragging
  • A dedicated drag handle
  • Interactive elements that do not initiate a drag
  • East and west resize handles
  • A callback containing the new layout

Resizing stopped being a feature we had to invent. Compaction stopped being a side effect we tried to coax out of CSS. Responsive layout stopped meaning “render the same array in fewer columns.” These concepts existed in the library and, just as importantly, in its data model.

Persistence became honest too. The old API saved only an item’s linear position:

{
  (id, position);
}

The new API saves the state users actually created:

{
  (id, position, x, y, w, h);
}

We still wrote application code. We added defaults for different tile types, converted the desktop layout into a single-column mobile layout, sanitized persisted values, debounced saves, and handled failed requests. RGL did not remove the need for engineering.

It removed the need to pretend we were not building a spatial layout.

Why AI Made the Wrong Path Feel Productive

This experience changed how I think about AI-assisted development.

AI is extremely good at local optimization. Give it a dnd-kit implementation and ask it to fix a drag bug, and it will search the space around dnd-kit: a different sorting strategy, a new collision detector, another sensor, a drag overlay, a transform adjustment, or a CSS workaround.

Those can all be reasonable answers to the prompt. They can also move the project farther down the wrong path.

The model usually treats the current architecture as a constraint unless I explicitly invite it to challenge that architecture. Each successful patch reinforces the premise that the existing tool should remain. The code gets more internally consistent while the distance between the abstraction and the product grows.

Once I changed the premise, AI became useful in a different way. With RGL selected, it could map our requirements onto concepts the library already exposed: LayoutItem, responsive layouts, width constraints, resize handles, compaction, and onLayoutChange. The implementation became easier because the search space finally contained the right primitives.

AI did not suddenly become smarter. I gave it a better problem.

dnd-kit Was Not the Mistake

It would be easy to turn this into “dnd-kit bad, RGL good,” but that is the wrong lesson.

We still use dnd-kit throughout the application. It is a great fit for reorderable questions, steps, sections, checklist items, accounts, and other ordered collections. In those features, dragging changes an item’s index. The browser can own the visual layout, and the application only needs to save the new order.

RGL would be excessive for those jobs.

The useful distinction is not list versus grid in the CSS sense. dnd-kit can absolutely sort a uniform grid. The distinction is ordered data versus spatial data.

Use dnd-kit when the result of the interaction is an ordered array. Use a layout engine such as RGL when the result is a set of coordinates and dimensions.

The Checklist I Wish I Had Started With

Before choosing a drag-and-drop library, I now ask:

  1. What state must be saved: an index, or x, y, w, and h?
  2. Can items span columns or have different heights?
  3. Can users resize items?
  4. Should nearby items move when one item changes size or position?
  5. Do gaps need to be compacted automatically?
  6. Does the layout change at responsive breakpoints?

If the first answer is “an index” and the rest are no, a sortable toolkit is probably the right abstraction.

If geometry, resizing, collision resolution, or compaction are part of the feature, I want a layout engine before I write a line of drag logic.

I also ask AI one question before implementation:

Based on the state this feature must persist and the interactions it must support, is the proposed library the right abstraction? Compare it with purpose-built alternatives before writing code.

That prompt costs almost nothing. It can save days of productive-looking work.

Step Back Before You Prompt Again

The warning sign in this project was not one dramatic failure. It was a series of small fixes that never made the feature feel natural.

When AI keeps producing code that is plausible but brittle, the next move is not always a more detailed prompt. Sometimes the most valuable thing an engineer can do is step back and reopen a decision that everyone, including the AI, has started treating as settled.

The speed of AI makes this more important, not less. It can build deep into a chosen abstraction before the friction forces us to reconsider. Our job is to notice when the friction is evidence.

In this case, one better question turned a growing collection of drag-and-drop workarounds into a layout model that matched the product. Once that happened, the code became easier for both the human and the AI to reason about.

The biggest improvement was not a better prompt or a better patch.

It was choosing a library that already understood the problem.


Further reading

Share :