【Vibe Coder 的 React 教學】#05 實戰:用 Vibe Coding 打造完整應用

測驗:Vibe Coder 的 React 教學 #05 實戰篇

共 5 題,點選答案後會立即顯示結果

1. 在待辦事項應用中,為什麼 todos 狀態要放在 App 元件裡,而不是放在 TodoList 元件裡?

  • A. 因為 App 是第一個被建立的元件
  • B. 因為 TodoInput、TodoFilter、TodoList 都需要用到這個資料,App 是它們的共同祖先
  • C. 因為 App 的效能比較好
  • D. 因為 React 規定狀態只能放在最上層元件

2. 以下程式碼中,e.preventDefault() 的作用是什麼?

const handleSubmit = (e) => { e.preventDefault(); if (text.trim()) { onAdd(text); setText(”); } };
  • A. 阻止 onAdd 函式被呼叫
  • B. 清空輸入框的內容
  • C. 阻止表單送出時頁面重新整理
  • D. 檢查 text 是否為空白

3. 刪除待辦事項時使用 todos.filter(todo => todo.id !== id),這行程式碼的邏輯是什麼?

  • A. 找出 id 等於傳入值的項目並刪除它
  • B. 建立新陣列,保留 id 不等於傳入值的所有項目
  • C. 直接從原陣列中移除指定項目
  • D. 將指定項目的 id 設為 null

4. 在篩選功能中,filteredTodostodos 的關係是什麼?

const filteredTodos = todos.filter(todo => { if (filter === ‘all’) return true; if (filter === ‘active’) return !todo.completed; if (filter === ‘completed’) return todo.completed; });
  • A. filteredTodos 會直接修改 todos 的內容
  • B. 兩者完全獨立,互不影響
  • C. todos 是 filteredTodos 的複製品
  • D. filteredTodos 是根據篩選條件過濾後的陣列,顯示時用它,但操作時改的是 todos

5. 根據 Vibe Coding 工作流程,當 AI 生成程式碼後,下一步應該做什麼?

  • A. 直接部署上線
  • B. 請 AI 再重寫一次
  • C. 讀懂程式碼在做什麼
  • D. 刪除不需要的檔案

這篇文章在幹嘛

帶你從零開始,用 Vibe Coding 的方式打造一個完整的待辦事項應用。我們會整合前四篇學到的所有概念:元件、Props、State、事件處理,並學會如何與 AI 協作開發。

學習目標

讀完這篇,你會知道:

  • 如何用 AI 討論功能需求、規劃專案
  • 怎麼把頁面拆分成可重用的元件
  • 狀態該放哪裡、怎麼管理
  • 完整實作新增、刪除、標記完成、篩選功能
  • 用 AI 生成 CSS 樣式
  • 程式碼怎麼組織才好維護

一、專案規劃:用 AI 討論功能需求

Vibe Coding 的第一步不是寫程式,而是和 AI 討論你想做什麼

你可以這樣問 AI

我想做一個待辦事項應用,需要這些功能:
1. 新增待辦事項
2. 標記完成/未完成
3. 刪除待辦事項
4. 篩選顯示(全部/進行中/已完成)

請幫我規劃元件結構

AI 可能這樣回答

建議元件結構:

App
├── Header          # 標題
├── TodoInput       # 輸入框 + 新增按鈕
├── TodoFilter      # 篩選按鈕(全部/進行中/已完成)
├── TodoList        # 待辦清單容器
│   └── TodoItem    # 單一待辦項目(可重複)
└── Footer          # 統計資訊
Code language: PHP (php)

翻譯:整個應用拆成 6 個元件,各司其職。

Vibe Coder 檢查點

和 AI 討論功能時確認:

  • [ ] 功能清單夠具體嗎?(「新增待辦」比「管理待辦」具體)
  • [ ] 有沒有遺漏的基本功能?
  • [ ] 元件拆分合理嗎?(一個元件做一件事)

二、元件設計:拆分頁面為可重用元件

最小範例:元件結構

// 整個應用的元件樹
function App() {
    return (
        <div>
            <Header />                  {/* 標題 */}
            <TodoInput />               {/* 輸入 */}
            <TodoFilter />              {/* 篩選 */}
            <TodoList>                  {/* 清單 */}
                <TodoItem />            {/* 項目 */}
            </TodoList>
        </div>
    );
}
Code language: JavaScript (javascript)

翻譯:App 是老大,負責組合所有子元件。

每個元件的職責

元件 職責 需要的資料
Header 顯示標題
TodoInput 接收使用者輸入 新增函式
TodoFilter 切換篩選條件 目前篩選狀態、切換函式
TodoList 顯示待辦清單 篩選後的待辦陣列
TodoItem 顯示單一待辦 待辦資料、完成/刪除函式

三、狀態管理:決定哪些資料放在哪裡

狀態設計

這個應用需要兩個狀態:

// 在 App 元件中
const [todos, setTodos] = useState([]);        // 所有待辦事項
const [filter, setFilter] = useState('all');   // 篩選條件:'all' | 'active' | 'completed'
Code language: JavaScript (javascript)

為什麼狀態放在 App?

          App(狀態在這裡)
         /    |    \
   TodoInput  TodoFilter  TodoList
                              |
                          TodoItem

規則:狀態放在「需要用到這個資料的元件們」的共同祖先。

因為 TodoInputTodoFilterTodoList 都需要用到待辦資料,它們的共同祖先是 App,所以狀態放在 App

待辦項目的資料結構

// 一個待辦長這樣
const todo = {
    id: 1,                    // 唯一識別碼
    text: "學會 React",       // 內容
    completed: false          // 是否完成
};

// 整個 todos 陣列
const todos = [
    { id: 1, text: "學會 React", completed: false },
    { id: 2, text: "做待辦應用", completed: true },
];
Code language: JavaScript (javascript)

四、功能實作

4.1 新增待辦

// TodoInput 元件
function TodoInput({ onAdd }) {                    // 接收新增函式
    const [text, setText] = useState('');          // 輸入框的內容

    const handleSubmit = (e) => {
        e.preventDefault();                        // 阻止表單重新整理
        if (text.trim()) {                        // 確認不是空白
            onAdd(text);                          // 呼叫新增函式
            setText('');                          // 清空輸入框
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                value={text}
                onChange={(e) => setText(e.target.value)}
                placeholder="輸入待辦事項..."
            />
            <button type="submit">新增</button>
        </form>
    );
}
Code language: JavaScript (javascript)
// App 元件中的新增函式
const addTodo = (text) => {
    const newTodo = {
        id: Date.now(),              // 用時間戳當 ID
        text: text,
        completed: false
    };
    setTodos([...todos, newTodo]);   // 展開舊陣列 + 新項目
};
Code language: JavaScript (javascript)

翻譯

  • TodoInput 管理「正在輸入什麼」
  • 按下新增時,呼叫 onAdd 把文字傳給 App
  • App 建立新物件,加到陣列尾端

4.2 刪除待辦

// App 元件中
const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
    //              ↑ 保留 id 不等於傳入 id 的項目
};
Code language: JavaScript (javascript)
// TodoItem 元件
function TodoItem({ todo, onDelete, onToggle }) {
    return (
        <li>
            <span>{todo.text}</span>
            <button onClick={() => onDelete(todo.id)}>刪除</button>
        </li>
    );
}
Code language: JavaScript (javascript)

翻譯filter 會建立新陣列,只保留條件為 true 的項目。刪除就是「保留 id 不符合的」。

4.3 標記完成

// App 元件中
const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
        todo.id === id
            ? { ...todo, completed: !todo.completed }   // 找到就切換
            : todo                                       // 沒找到就保持原樣
    ));
};
Code language: JavaScript (javascript)
// TodoItem 元件
function TodoItem({ todo, onDelete, onToggle }) {
    return (
        <li>
            <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => onToggle(todo.id)}
            />
            <span style={{
                textDecoration: todo.completed ? 'line-through' : 'none'
            }}>
                {todo.text}
            </span>
            <button onClick={() => onDelete(todo.id)}>刪除</button>
        </li>
    );
}
Code language: JavaScript (javascript)

翻譯

  • map 遍歷每個項目
  • 找到要切換的,用展開複製並改 completed
  • 其他的保持不變

4.4 篩選功能

// App 元件中
const [filter, setFilter] = useState('all');

// 根據篩選條件過濾
const filteredTodos = todos.filter(todo => {
    if (filter === 'all') return true;           // 全部
    if (filter === 'active') return !todo.completed;   // 進行中
    if (filter === 'completed') return todo.completed;  // 已完成
});
Code language: JavaScript (javascript)
// TodoFilter 元件
function TodoFilter({ filter, onFilterChange }) {
    return (
        <div>
            <button
                onClick={() => onFilterChange('all')}
                style={{ fontWeight: filter === 'all' ? 'bold' : 'normal' }}
            >
                全部
            </button>
            <button
                onClick={() => onFilterChange('active')}
                style={{ fontWeight: filter === 'active' ? 'bold' : 'normal' }}
            >
                進行中
            </button>
            <button
                onClick={() => onFilterChange('completed')}
                style={{ fontWeight: filter === 'completed' ? 'bold' : 'normal' }}
            >
                已完成
            </button>
        </div>
    );
}
Code language: PHP (php)

翻譯

  • filteredTodos 是根據 filter 過濾後的陣列
  • 顯示時用 filteredTodos,但操作時改的是原始 todos

五、完整程式碼

App.jsx(主元件)

import { useState } from 'react';
import TodoInput from './TodoInput';
import TodoFilter from './TodoFilter';
import TodoList from './TodoList';
import './App.css';

function App() {
    // 狀態
    const [todos, setTodos] = useState([]);
    const [filter, setFilter] = useState('all');

    // 新增
    const addTodo = (text) => {
        setTodos([...todos, {
            id: Date.now(),
            text,
            completed: false
        }]);
    };

    // 刪除
    const deleteTodo = (id) => {
        setTodos(todos.filter(todo => todo.id !== id));
    };

    // 切換完成
    const toggleTodo = (id) => {
        setTodos(todos.map(todo =>
            todo.id === id
                ? { ...todo, completed: !todo.completed }
                : todo
        ));
    };

    // 篩選
    const filteredTodos = todos.filter(todo => {
        if (filter === 'all') return true;
        if (filter === 'active') return !todo.completed;
        if (filter === 'completed') return todo.completed;
    });

    // 統計
    const activeCount = todos.filter(t => !t.completed).length;

    return (
        <div className="app">
            <h1>My Todos</h1>
            <TodoInput onAdd={addTodo} />
            <TodoFilter filter={filter} onFilterChange={setFilter} />
            <TodoList
                todos={filteredTodos}
                onToggle={toggleTodo}
                onDelete={deleteTodo}
            />
            <p>{activeCount} 項待完成</p>
        </div>
    );
}

export default App;
Code language: JavaScript (javascript)

TodoInput.jsx

import { useState } from 'react';

function TodoInput({ onAdd }) {
    const [text, setText] = useState('');

    const handleSubmit = (e) => {
        e.preventDefault();
        if (text.trim()) {
            onAdd(text);
            setText('');
        }
    };

    return (
        <form onSubmit={handleSubmit} className="todo-input">
            <input
                value={text}
                onChange={(e) => setText(e.target.value)}
                placeholder="輸入待辦事項..."
            />
            <button type="submit">新增</button>
        </form>
    );
}

export default TodoInput;
Code language: JavaScript (javascript)

TodoFilter.jsx

function TodoFilter({ filter, onFilterChange }) {
    const buttons = [
        { key: 'all', label: '全部' },
        { key: 'active', label: '進行中' },
        { key: 'completed', label: '已完成' }
    ];

    return (
        <div className="todo-filter">
            {buttons.map(btn => (
                <button
                    key={btn.key}
                    onClick={() => onFilterChange(btn.key)}
                    className={filter === btn.key ? 'active' : ''}
                >
                    {btn.label}
                </button>
            ))}
        </div>
    );
}

export default TodoFilter;
Code language: JavaScript (javascript)

TodoList.jsx

import TodoItem from './TodoItem';

function TodoList({ todos, onToggle, onDelete }) {
    if (todos.length === 0) {
        return <p className="empty">沒有待辦事項</p>;
    }

    return (
        <ul className="todo-list">
            {todos.map(todo => (
                <TodoItem
                    key={todo.id}
                    todo={todo}
                    onToggle={onToggle}
                    onDelete={onDelete}
                />
            ))}
        </ul>
    );
}

export default TodoList;
Code language: JavaScript (javascript)

TodoItem.jsx

function TodoItem({ todo, onToggle, onDelete }) {
    return (
        <li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
            <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => onToggle(todo.id)}
            />
            <span className="todo-text">{todo.text}</span>
            <button
                onClick={() => onDelete(todo.id)}
                className="delete-btn"
            >
                刪除
            </button>
        </li>
    );
}

export default TodoItem;
Code language: JavaScript (javascript)

六、樣式美化:用 AI 生成 CSS

你可以這樣問 AI

請幫我生成這個待辦應用的 CSS,要求:
- 簡潔現代風格
- 響應式設計
- 完成的項目要有刪除線和灰色

AI 可能生成的 CSS

/* App.css */
.app {
    max-width: 500px;
    margin: 40px auto;
    padding: 20px;
    font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}

h1 {
    text-align: center;
    color: #333;
}

/* 輸入區 */
.todo-input {
    display: flex;
    gap: 10px;
    margin-bottom: 20px;
}

.todo-input input {
    flex: 1;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 16px;
}

.todo-input button {
    padding: 10px 20px;
    background: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

/* 篩選按鈕 */
.todo-filter {
    display: flex;
    gap: 10px;
    margin-bottom: 20px;
}

.todo-filter button {
    padding: 8px 16px;
    border: 1px solid #ddd;
    background: white;
    border-radius: 4px;
    cursor: pointer;
}

.todo-filter button.active {
    background: #333;
    color: white;
}

/* 清單 */
.todo-list {
    list-style: none;
    padding: 0;
}

.todo-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px;
    border-bottom: 1px solid #eee;
}

.todo-item.completed .todo-text {
    text-decoration: line-through;
    color: #999;
}

.todo-text {
    flex: 1;
}

.delete-btn {
    padding: 5px 10px;
    background: #ff4444;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

.empty {
    text-align: center;
    color: #999;
}
Code language: CSS (css)

七、程式碼組織:資料夾結構與命名

建議的專案結構

src/
├── App.jsx              # 主元件
├── App.css              # 主樣式
├── main.jsx             # 進入點
└── components/          # 元件資料夾
    ├── TodoInput.jsx
    ├── TodoFilter.jsx
    ├── TodoList.jsx
    └── TodoItem.jsx
Code language: PHP (php)

命名慣例

類型 慣例 範例
元件檔案 PascalCase TodoItem.jsx
元件函式 PascalCase function TodoItem()
一般變數 camelCase const filteredTodos
事件處理 handle/on 開頭 handleSubmit, onDelete
狀態更新 set 開頭 setTodos

當你請 AI 重構時

請幫我把這些元件搬到 components 資料夾,並更新 import 路徑
Code language: JavaScript (javascript)

八、Vibe Coding 總結:持續學習的心法

Vibe Coding 工作流程

1. 告訴 AI 你要做什麼
   ↓
2. AI 生成代碼
   ↓
3. 讀懂代碼在幹嘛(這系列教你的!)
   ↓
4. 測試功能
   ↓
5. 請 AI 修改/優化
   ↓
6. 重複 3-5

如何越來越強

階段 你會做的事
初期 看懂 AI 寫什麼,能判斷對不對
中期 能指出問題,請 AI 修改特定部分
後期 自己改小東西,大架構讓 AI 處理

和 AI 協作的技巧

好的提問

這個 TodoList 元件,當 todos 是空陣列時會顯示什麼?
可以加一個「沒有待辦事項」的提示嗎?

比較不好的提問

幫我改好一點

一句話:越具體的問題,得到越好的答案。

本系列重點回顧

篇章 核心概念
#01 元件就是「可重複使用的積木」
#02 Props 是「從外面傳進來的設定」
#03 State 是「元件自己記住的資料」
#04 事件處理是「使用者做了什麼,程式怎麼反應」
#05 把以上全部組合起來,做出完整應用

Vibe Coder 檢查點

看完這篇,你應該能夠:

  • [ ] 看懂一個完整 React 應用的結構
  • [ ] 理解元件之間如何傳遞資料和函式
  • [ ] 知道狀態該放在哪個元件
  • [ ] 看懂 filtermap 等陣列操作
  • [ ] 能和 AI 討論功能需求並看懂生成的代碼

下一步

恭喜你完成這個系列!接下來你可以:

  1. 動手做:請 AI 幫你建立這個待辦應用,邊做邊對照本文
  2. 加功能:試著請 AI 加入編輯功能、本地儲存等
  3. 做自己的專案:把學到的應用在你想做的東西上

記住 Vibe Coder 的核心:不用每行都會寫,但要能看懂 AI 在幹嘛

Happy Vibe Coding!

進階測驗:Vibe Coder 的 React 教學 #05 實戰篇

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在開發待辦應用,需要實作「編輯待辦內容」功能。目前 todos 狀態放在 App 元件中。你應該把編輯函式放在哪裡? 情境題

  • A. 放在 TodoItem 元件中,因為編輯的是單一項目
  • B. 放在 App 元件中,透過 Props 傳給 TodoItem
  • C. 放在 TodoList 元件中,統一管理清單
  • D. 建立新的 EditContext 來管理編輯狀態

2. 小明寫了以下程式碼來切換待辦完成狀態,但發現點擊後畫面沒有更新。問題出在哪裡? 錯誤診斷

const toggleTodo = (id) => { todos.forEach(todo => { if (todo.id === id) { todo.completed = !todo.completed; } }); };
  • A. forEach 無法修改陣列元素
  • B. 應該用 todo.id == id 而非 ===
  • C. 直接修改原陣列不會觸發 React 重新渲染,應該用 setTodos 設定新陣列
  • D. completed 屬性不存在

3. 你想為待辦應用加入「清除所有已完成項目」的功能按鈕。最佳的實作方式是? 情境題

  • A. 在 App 中新增 clearCompleted 函式,使用 setTodos(todos.filter(t => !t.completed))
  • B. 用 forEach 遍歷 todos,對每個已完成項目呼叫 deleteTodo
  • C. 直接設定 todos = todos.filter(t => !t.completed)
  • D. 建立新的 clearedTodos 狀態來追蹤被清除的項目

4. 小美的 TodoInput 元件在送出後沒有清空輸入框。以下是她的程式碼,問題在哪裡? 錯誤診斷

function TodoInput({ onAdd }) { const [text, setText] = useState(”); const handleSubmit = (e) => { e.preventDefault(); onAdd(text); text = ”; }; return ( <form onSubmit={handleSubmit}> <input value={text} onChange={(e) => setText(e.target.value)} /> <button type=”submit”>新增</button> </form> ); }
  • A. onAdd 函式沒有正確接收 text 參數
  • B. 應該用 setText('') 而非直接賦值 text = ''
  • C. e.preventDefault() 阻止了清空動作
  • D. input 的 value 屬性設定錯誤

5. 你想請 AI 幫你優化待辦應用的效能。以下哪個提問方式最有效? 情境題

  • A. 「幫我改好一點」
  • B. 「這個應用有問題」
  • C. 「效能不好,幫我處理」
  • D. 「當 todos 陣列很長時,每次篩選都要重新計算。可以用 useMemo 優化 filteredTodos 嗎?」

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *