測驗:Vibe Coder 的 React 教學 #05 實戰篇
共 5 題,點選答案後會立即顯示結果
1. 在待辦事項應用中,為什麼 todos 狀態要放在 App 元件裡,而不是放在 TodoList 元件裡?
2. 以下程式碼中,e.preventDefault() 的作用是什麼?
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
onAdd(text);
setText(”);
}
};
3. 刪除待辦事項時使用 todos.filter(todo => todo.id !== id),這行程式碼的邏輯是什麼?
4. 在篩選功能中,filteredTodos 和 todos 的關係是什麼?
const filteredTodos = todos.filter(todo => {
if (filter === ‘all’) return true;
if (filter === ‘active’) return !todo.completed;
if (filter === ‘completed’) return todo.completed;
});
5. 根據 Vibe Coding 工作流程,當 AI 生成程式碼後,下一步應該做什麼?
這篇文章在幹嘛
帶你從零開始,用 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
規則:狀態放在「需要用到這個資料的元件們」的共同祖先。
因為 TodoInput、TodoFilter、TodoList 都需要用到待辦資料,它們的共同祖先是 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 應用的結構
- [ ] 理解元件之間如何傳遞資料和函式
- [ ] 知道狀態該放在哪個元件
- [ ] 看懂
filter、map等陣列操作 - [ ] 能和 AI 討論功能需求並看懂生成的代碼
下一步
恭喜你完成這個系列!接下來你可以:
- 動手做:請 AI 幫你建立這個待辦應用,邊做邊對照本文
- 加功能:試著請 AI 加入編輯功能、本地儲存等
- 做自己的專案:把學到的應用在你想做的東西上
記住 Vibe Coder 的核心:不用每行都會寫,但要能看懂 AI 在幹嘛。
Happy Vibe Coding!
進階測驗:Vibe Coder 的 React 教學 #05 實戰篇
測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。
共 5 題,包含情境題與錯誤診斷題。
1. 你正在開發待辦應用,需要實作「編輯待辦內容」功能。目前 todos 狀態放在 App 元件中。你應該把編輯函式放在哪裡? 情境題
2. 小明寫了以下程式碼來切換待辦完成狀態,但發現點擊後畫面沒有更新。問題出在哪裡? 錯誤診斷
const toggleTodo = (id) => {
todos.forEach(todo => {
if (todo.id === id) {
todo.completed = !todo.completed;
}
});
};
3. 你想為待辦應用加入「清除所有已完成項目」的功能按鈕。最佳的實作方式是? 情境題
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>
);
}