跪拜 Guibai
← Back to the summary

Stop Letting AI Guess Your Architecture: Plan First, Then Glue the Parts

Vibe Coding Crash Record: Why Can Others' AI-Written Code Run While Yours Can't Be Changed?


Opening: Two Prompts, Two Lives

❌ Vibe Newbie (One Command to Rule Them All):

Write me a React to-do list page that supports adding and deleting tasks.

The AI starts outputting code rapidly. You copy and paste, run npm run dev — errors. You fix 3 undefined values, then hit 2 more type errors. It finally runs, but all the code is crammed into a single 300-line App.jsx. You want to add drag-and-drop sorting but have no idea where to start.

✅ Vibe Veteran (Plan First, Then Code):

Follow the glue programming mindset: prioritize mature solutions, avoid creating logic from scratch.

Phase 1: Only do planning, outputting any code is forbidden.

1. Confirm the tech stack: React 19 + Tailwind CSS + TypeScript
2. Define feature boundaries:
   - Add to-dos, delete to-dos, toggle completion status
   - No local persistence, filtering, or drag-and-drop features
3. Split modules (Lego components):
   Input component, to-do item component, list container component
4. Define data flow:
   useState stores a task array, data structure { id, text, completed }
5. Output this complete plan and wait for my confirmation before implementing the code in segments.

The AI outputs a clear planning document. You glance at it and think, "No, I do want drag-and-drop sorting," and add it to the plan. The AI re-plans. You confirm again. Then the AI starts writing code — this time, the code it outputs has clear component boundaries, pre-agreed data structures, and no file exceeds 50 lines.

This article uses the second method to walk you through a complete React TodoList project. After reading, you can apply the same method to your own projects.


Core Concept: Vibe Coding Isn't "Lie Down and Let AI Write"

The essence of Vibe Coding is humans make decisions, AI does the execution. You are responsible for the "what" and the "why," and the AI is responsible for the "how to write it."

But most people's problem is — they hand the "what" over to the AI too. The AI doesn't know if your project needs local storage, doesn't know which drag-and-drop library you want to use, doesn't know if you prefer Composition or Render Props. It can only guess. If it guesses wrong, that's your bug.

This leads to the core methodology of this article — the three-step method:

Step 1: Draw the blueprint first (planning first) → Define boundaries to prevent the AI from improvising
Step 2: Then lay the bricks (glue programming) → Only glue mature components together, don't hand-code low-level logic
Step 3: Let the AI evolve (meta-methodology) → Let the AI help you optimize your prompts

Use an analogy to understand the first two steps:

You wouldn't let a new colleague start writing code immediately. You'd first show them the technical documentation, define the requirement boundaries, and tell them which wheels have already been built and can be used directly. The AI is the same — treat it like a new colleague, not a fortune teller.

Below, we'll walk through the first two steps using a React TodoList project.


Step 1: Planning First — 5 Minutes Drawing Saves 2 Hours of Bug Fixing

Why This Step Is "Mandatory" and Not "Optional"

What AI is good at is not "deciding what to do," but "executing what has been decided." When you skip planning and let it write code directly, it will improvise in the following areas:

What You Didn't Say in the Prompt What the AI Might Do Consequence
What the data structure looks like Invent its own field names like title vs name vs text All inter-component parameter passing breaks
Whether to split components Cram everything into App.jsx 300 lines of code that can't be refactored
Whether to use local storage Arbitrarily add localStorage Unexpected logic appears that you didn't anticipate
Which drag-and-drop library to use Hand-code a drag-and-drop system More bugs than features

The essence of planning is to constrain the AI's output with documentation. It's like adding TypeScript types to a function — once the types are defined, parameters won't be passed randomly.

A Planning Prompt Template You Can Copy Directly

Follow the glue programming mindset: prioritize mature solutions, avoid creating logic from scratch.

Phase 1: Only do planning, outputting any code is forbidden.

1. Confirm the tech stack: [Fill in your tech stack, e.g., React 19 + Tailwind CSS + TypeScript]

2. Define feature boundaries:
   - [Feature to do 1], [Feature to do 2]
   - Explicitly exclude: [Unwanted features], to prevent the AI from adding them arbitrarily

3. Split modules (Lego components):
   [List your expected component tree]

4. Define data flow:
   - State management solution: [useState / useReducer / Zustand]
   - Core data structure: [interface/type definition]

5. Output this complete plan and wait for my confirmation before implementing the code in segments.

Running It for Real: The TodoList Plan Output

Filling the above template with the TodoList requirements, the AI output this plan:

Tech Stack: React 19 + Tailwind CSS 4 + TypeScript + Vite
Feature Boundaries:
  ✅ Add to-dos, delete to-dos, toggle completion status
  ❌ No local persistence, no filtering, no drag-and-drop (first version)
Module Split:
  TodoInput   → Input box + Add button
  TodoItem    → Single to-do item (checkbox + delete)
  TodoList    → List container (iteration + state management)
  TodoEmpty   → Empty state placeholder
Data Flow:
  useState<Todo[]> stored in App.tsx
  Todo = { id: number; text: string; completed: boolean }

⚠️ Here's a counter-intuitive point: Define the data structure during the planning phase. The three fields { id, text, completed } are decided by you, not guessed by the AI. If you don't define them first, the AI might write { title, isDone }, and then you'll get an undefined error when using todo.text in a child component.

After confirming the plan is correct, move to step two.


Step 2: Glue Programming — Code Is Glued Together, Not Built from Scratch

A One-Sentence Definition

Glue Programming = If you can copy it, don't write it; if you can connect it, don't build it. The AI doesn't create parts; it's only responsible for gluing existing parts together. You only write the glue code for connection, invocation, and data flow.

Comparing two approaches:

Hand-Build Parts (❌) Glue Programming (✅)
Drag-and-Drop Sorting Hand-code mousedown/mousemove/mouseup + coordinate calculation + sorting algorithm pnpm add @hello-pangea/dnd, glue DragDropContext
Styling Hand-write 200 lines of CSS Tailwind utility classes
State Hand-write pub/sub React useState

Core principle: The code you write should be the "glue" connecting one mature component to another, not the components themselves.

Below, we'll build the TodoList step-by-step in construction order.

Step 2.1: Build the Skeleton — Type Definitions + App Entry

First, define the data structure. This is the interface specification for the "glue" — all components collaborate around this interface:

// src/types.ts
// 🔑 Key: The data structure was decided during the planning phase; here it's implemented
export interface Todo {
  id: number
  text: string
  completed: boolean
}

Main App component — state is lifted to the top level, child components are only responsible for rendering and callbacks:

// src/App.tsx
import { useState } from 'react'
import type { Todo } from './types'
import TodoInput from './components/TodoInput'
import TodoList from './components/TodoList'

function App() {
  const [todos, setTodos] = useState<Todo[]>([])

  // 🔑 Key: All state operations are defined at the App layer; child components only "notify"
  const handleAdd = (text: string) => {
    const newTodo: Todo = { id: Date.now(), text, completed: false }
    setTodos((prev) => [newTodo, ...prev])
    // ⚠️ Using functional update (prev) => [] here instead of setTodos([...todos, newTodo])
    // because setTodos([...todos, ...]) might read a stale closure during rapid consecutive additions
  }

  const handleToggle = (id: number) => {
    setTodos((prev) =>
      prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
    )
  }

  const handleDelete = (id: number) => {
    setTodos((prev) => prev.filter((t) => t.id !== id))
  }

  return (
    <div className="min-h-screen bg-gray-50 flex justify-center px-4 pt-16">
      <div className="w-full max-w-lg">
        <h1 className="text-3xl font-bold text-center text-gray-800 mb-8">
          📋 Todo List
        </h1>
        <TodoInput onAdd={handleAdd} />
        <TodoList
          todos={todos}
          onToggle={handleToggle}
          onDelete={handleDelete}
          onReorder={handleReorder}
        />
      </div>
    </div>
  )
}

This is what "glue" means — App.tsx itself doesn't write any UI interaction logic. It only does three things: defines data, defines operation functions, and passes functions to child components. Rendering details are all delegated to the children.

Step 2.2: Glue the First Component — TodoInput

// src/components/TodoInput.tsx
import { useState, useRef } from 'react'

interface Props {
  onAdd: (text: string) => void
}

export default function TodoInput({ onAdd }: Props) {
  const [value, setValue] = useState('')
  const inputRef = useRef<HTMLInputElement>(null)

  const handleSubmit = () => {
    const trimmed = value.trim()
    if (!trimmed) return  // 🔑 Don't submit blanks, a UX detail
    onAdd(trimmed)
    setValue('')
    inputRef.current?.focus()  // 🔑 Auto-focus after submit, keeps flow uninterrupted for continuous adding
  }

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter') handleSubmit()
  }

  return (
    <div className="flex gap-2 mb-6">
      <input
        ref={inputRef}
        type="text"
        className="flex-1 px-4 py-3 border border-gray-300 rounded-lg
                   focus:outline-none focus:ring-2 focus:ring-indigo-400
                   focus:border-transparent text-gray-700 placeholder-gray-400"
        placeholder="Enter a new task, press Enter to add..."
        value={value}
        onChange={(e) => setValue(e.target.value)}
        // ⚠️ e.target's type is EventTarget, not HTMLInputElement
        // but in React, onChange's generic is correctly inferred, so e.target.value can be used directly here
        onKeyDown={handleKeyDown}
        autoFocus
      />
      <button
        className="px-6 py-3 bg-indigo-500 text-white font-medium rounded-lg
                   hover:bg-indigo-600 active:scale-95 transition-all
                   disabled:opacity-40 disabled:cursor-not-allowed"
        onClick={handleSubmit}
        disabled={!value.trim()}  // 🔑 Disable button when blank, defensive UI
      >
        Add
      </button>
    </div>
  )
}

The "glue volume" of this component is very low — it only glues together two native HTML elements, <input> and <button>, plus the boilerplate for controlled state. No wheels were invented.

Step 2.3: Glue the Empty State — TodoEmpty

Extract a separate empty state component instead of writing {todos.length === 0 && <p>None</p>} inside TodoList:

// src/components/TodoEmpty.tsx
export default function TodoEmpty() {
  return (
    <div className="text-center py-12 text-gray-400">
      <p className="text-5xl mb-4">📝</p>
      <p className="text-lg">No tasks yet, go add one 🎯</p>
    </div>
  )
}

Why extract a separate component for just 8 lines? Because "what to display when the list is empty" and "how to render when the list is not empty" are two independent concerns. Mixed together, if you want to change the empty state text or style later, you'd have to search through a large chunk of logic. Glue programming advocates that "each component only cares about one thing."

Step 2.4: Glue the List Container + Drag-and-Drop — The Essence of Glue Programming

This step best embodies the glue mindset. The requirement is to "add drag-and-drop sorting to the to-do list."

Wrong approach: "Write me a drag-and-drop sorting feature for a React to-do list" — The AI will likely hand-code a set of mousedown/mousemove/mouseup + coordinate listeners + sorting algorithms. Hand-coding drag-and-drop has extremely many edge cases, purely planting landmines for yourself.

Glue approach:

Follow the glue programming principle: Never develop low-level logic from scratch; prioritize mature open-source components long-validated by the community.

1. First, research: Mature drag-and-drop libraries in the React ecosystem, prioritize @hello-pangea/dnd
   (An active fork of react-beautiful-dnd, widely used in the industry)
   pnpm add @hello-pangea/dnd

2. Don't write any low-level drag-and-drop code, only do the gluing work:
   Glue the existing TodoList component together with @hello-pangea/dnd

3. Only write the glue code for adaptation between modules and data flow

The code the AI provides is pure "glue":

// src/components/TodoList.tsx
import type { DropResult } from '@hello-pangea/dnd'
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'
import type { Todo } from '../types'
import TodoItem from './TodoItem'
import TodoEmpty from './TodoEmpty'

interface Props {
  todos: Todo[]
  onToggle: (id: number) => void
  onDelete: (id: number) => void
  onReorder: (result: DropResult) => void  // 🔑 Drag event passed to App layer for handling
}

export default function TodoList({ todos, onToggle, onDelete, onReorder }: Props) {
  if (todos.length === 0) {
    return <TodoEmpty />
  }

  return (
    // 🔑 DragDropContext: The "power switch" for the entire drag-and-drop system
    <DragDropContext onDragEnd={onReorder}>
      {/* 🔑 Droppable: The "destination" for drag-and-drop, an area where things can be dropped */}
      <Droppable droppableId="todos">
        {(provided) => (
          <ul
            ref={provided.innerRef}
            // ⚠️ Must spread provided.droppableProps here, otherwise drag-and-drop won't detect the drop zone
            {...provided.droppableProps}
            className="flex flex-col gap-2"
          >
            {todos.map((todo, index) => (
              // 🔑 Draggable: Each individual "part" that can be dragged
              <Draggable key={todo.id} draggableId={todo.id.toString()} index={index}>
                {(provided, snapshot) => (
                  <TodoItem
                    todo={todo}
                    onToggle={onToggle}
                    onDelete={onDelete}
                    isDragging={snapshot.isDragging}
                    // 🔑 The following three props are the "glue" — passing the dnd library's capabilities to the pure presentational component
                    dragHandleProps={provided.dragHandleProps}
                    innerRef={provided.innerRef}
                    draggableProps={provided.draggableProps}
                  />
                )}
              </Draggable>
            ))}
            {provided.placeholder}
            {/* ⚠️ Don't forget the placeholder! Otherwise the list height will collapse during dragging */}
          </ul>
        )}
      </Droppable>
    </DragDropContext>
  )
}

You didn't write a single line of drag-and-drop logic. Coordinate calculation, animation, sorting — it's all @hello-pangea/dnd's job. What you wrote is just connecting DragDropContext, Droppable, Draggable, and your own TodoItem. This is glue programming.

Step 2.5: Glue the Last Component — TodoItem

// src/components/TodoItem.tsx
import type {
  DraggableProvidedDragHandleProps,
  DraggableProvidedDraggableProps,
} from '@hello-pangea/dnd'
import type { Todo } from '../types'

interface Props {
  todo: Todo
  onToggle: (id: number) => void
  onDelete: (id: number) => void
  // 🔑 The following three are injected by the parent Draggable; TodoItem is only responsible for "gluing"
  isDragging: boolean
  dragHandleProps: DraggableProvidedDragHandleProps | null
  innerRef: (element: HTMLElement | null) => void
  draggableProps: DraggableProvidedDraggableProps
}

export default function TodoItem({
  todo, onToggle, onDelete,
  isDragging, dragHandleProps, innerRef, draggableProps,
}: Props) {
  return (
    <li
      ref={innerRef}
      // ⚠️ Spread order: draggableProps first, then dragHandleProps
      // Reversing the order will break the drag handle — dragHandleProps needs to override some behaviors of draggableProps
      {...draggableProps}
      {...dragHandleProps}
      className={`flex items-center gap-3 px-4 py-3 rounded-lg border
                   transition-all group
                   ${isDragging ? 'shadow-lg ring-2 ring-indigo-300' : ''}
                   ${todo.completed
                     ? 'bg-gray-100 border-gray-200'
                     : 'bg-white border-gray-200 hover:border-indigo-300 hover:shadow-sm'
                   }`}
    >
      {/* Checkbox button */}
      <button
        className={`w-5 h-5 rounded-full border-2 flex items-center justify-center
                     flex-shrink-0 transition-colors cursor-pointer
                     ${todo.completed
                       ? 'bg-indigo-500 border-indigo-500'
                       : 'border-gray-300 hover:border-indigo-400'
                     }`}
        onClick={() => onToggle(todo.id)}
      >
        {todo.completed && (
          <svg width="12" height="12" viewBox="0 0 14 14" fill="none">
            <path d="M11.5 3.5L5.5 10.5L2.5 7.5"
              stroke="white" strokeWidth="2" fill="none"
              strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        )}
      </button>

      {/* Task text */}
      <span
        className={`flex-1 cursor-pointer select-none
                     ${todo.completed ? 'line-through text-gray-400' : 'text-gray-700'}`}
        onClick={() => onToggle(todo.id)}
      >
        {todo.text}
      </span>

      {/* Delete button — only visible on hover */}
      <button
        className="text-gray-300 hover:text-red-500 transition-colors
                   opacity-0 group-hover:opacity-100 cursor-pointer"
        onClick={() => onDelete(todo.id)}
      >
        ✕
      </button>
    </li>
  )
}

⚠️ The spread order of {...draggableProps} and {...dragHandleProps} matters. dragHandleProps overrides some drag behaviors, turning "the whole row is draggable" into "the whole row acts as a drag handle." If the order is reversed, dragging might not work at all.

Step 2.6: Add the Drag Callback in the App Layer

Back in App.tsx, add the final piece of glue:

// Append inside the App component
import type { DropResult } from '@hello-pangea/dnd'

const handleReorder = (result: DropResult) => {
  // ⚠️ Must check for null: destination is null when dragged outside the list
  if (!result.destination) return

  const items = Array.from(todos)  // 🔑 Copy first then operate, immutability principle
  const [moved] = items.splice(result.source.index, 1)
  items.splice(result.destination.index, 0, moved)
  setTodos(items)
}

// Pass to TodoList:
<TodoList
  todos={todos}
  onToggle={handleToggle}
  onDelete={handleDelete}
  onReorder={handleReorder}  // ← New
/>

Done. The complete project structure — 7 files, none exceeding 55 lines:

src/
├── types.ts            ← Data structure (decided during planning)
├── App.tsx             ← Glue hub, state + callbacks
├── main.tsx            ← ReactDOM entry
├── index.css           ← Tailwind import (just one line @import "tailwindcss")
└── components/
    ├── TodoInput.tsx    ← Input box glue
    ├── TodoList.tsx     ← List + drag-and-drop glue
    ├── TodoItem.tsx     ← Single item rendering
    └── TodoEmpty.tsx    ← Empty state

Summary: Next Time You Vibe Code, Copy This Checklist

Back to the opening pain point: Why can't your AI-written code be changed? Because you handed all decision-making power to the AI. The AI guessed 90% correctly, and the remaining 10% is the root cause of why you can't modify it.

The Three-Step Checklist — Go through this every time before letting AI write code:

Step What to Do One-Sentence Mnemonic
① Planning First Tech stack → Feature boundaries → Module split → Data structure "Draw the blueprint before laying bricks"
② Glue Programming If you can copy it, don't write it; only write the glue code between components "You're not a wheel maker, you're the mortar mixer"
③ Meta-Methodology Let the AI optimize your prompts "Teach the AI how to teach you"

Quick Reference Sheet (Screenshot-friendly):

✅ Define interface/type during the planning phase; don't let the AI guess field names
✅ Clearly write out "what to do" and "what not to do" in feature boundaries
✅ Before installing any third-party library, confirm it's community-validated (downloads, stars, maintenance status)
✅ Prefer functional updates for setState: (prev) => newState
✅ For drag-and-drop libraries, only do gluing — DragDropContext → Droppable → Draggable → Your component
✅ Component splitting principle: One component only cares about one thing
✅ Write {...draggableProps} before {...dragHandleProps}
❌ Don't let the AI hand-code low-level logic like drag-and-drop, form validation, animations
❌ Don't directly merge the first version of AI-generated code into your project

Next steps you can take:

  1. Apply the prompt template from this article to your own project; first let the AI produce a plan for you to review.
  2. Try using the glue mindset to refactor an existing "spaghetti code" component — first ask yourself, "Can any existing library replace this logic?"
  3. Advanced: Research "meta-methodology" — using AI to optimize your AI coding prompts (this topic is enough for another article).

An open question: Planning first solves the problem of "the AI doesn't know what to do," but what if you yourself don't know how to technically break things down? Should you first ask the AI "how to make a plan"? Feel free to discuss your approach in the comments.


Juejin Category Suggestions: