DAY5(2025/06/18) [TOC] # DAY5(2025/06/18)React ## 進度日誌 * 開始 **API 串接 & useEffect 資料自動載入** * 成功串接 JSONPlaceholder Todos API,自動顯示前 10 筆資料 * 熟悉 `fetch`/`useEffect` 非同步流程、TypeScript 型別、loading/error UI 處理 ## 筆記區 📘 React ### ▶ API 串接 & useEffect 資料自動載入 * `fetch`(或 `axios`)從外部 API 拿資料 * `useEffect`:元件掛載時自動抓資料 * 資料型別安全處理(TypeScript 型別) * 處理「載入中」、「錯誤」等 UI 狀態 --- #### 1️⃣ 小專案目標 串接「JSONPlaceholder」的 **Todos API**,自動顯示一串任務清單! API 位置:[https://jsonplaceholder.typicode.com/todos](https://jsonplaceholder.typicode.com/todos) --- ##### 📦 基本流程 1. 建立一個新元件(或在 `App.tsx` 練習) 2. 用 `useEffect` 控制元件掛載時自動抓資料 3. 用 `useState` 管理「todos 資料」、「載入狀態」、「錯誤狀態」 4. 把資料顯示在畫面上 --- #### 2️⃣ 練習程式碼:`FetchTodos.tsx` ```tsx import React, { useState, useEffect } from 'react'; // 定義 API 回傳的資料型別 type Todo = { userId: number; id: number; title: string; completed: boolean; }; const FetchTodos: React.FC = () => { const [todos, setTodos] = useState<Todo[]>([]); const [loading, setLoading] = useState<boolean>(true); const [error, setError] = useState<string>(''); useEffect(() => { setLoading(true); fetch('https://jsonplaceholder.typicode.com/todos') .then(res => { if (!res.ok) throw new Error('API 回傳失敗'); return res.json(); }) .then((data: Todo[]) => setTodos(data)) .catch(err => setError(err.message)) .finally(() => setLoading(false)); }, []); if (loading) return <p>載入中...</p>; if (error) return <p style={{ color: 'red' }}>錯誤:{error}</p>; return ( <div> <h2>API Todo List (前 10 筆)</h2> <ul> {todos.slice(0, 10).map(todo => ( <li key={todo.id}> <input type="checkbox" checked={todo.completed} readOnly /> {todo.title} </li> ))} </ul> </div> ); }; export default FetchTodos; ``` --- ![image](https://hackmd.io/_uploads/BJ6CrQBVex.png) ![image](https://hackmd.io/_uploads/Hk_lI7S4le.png) ![image](https://hackmd.io/_uploads/S1u-LQSEle.png) ![image](https://hackmd.io/_uploads/rJKGUmrVlx.png) --- #### 3️⃣ 在 `App.tsx` 引用 ```tsx import FetchTodos from './components/FetchTodos'; function App() { return ( <div> <h1>API 串接範例</h1> <FetchTodos /> </div> ); } ``` --- #### 成果 * `useEffect`:**元件初次渲染自動抓資料** * `fetch` + `.then/.catch` 處理 API 資料流 * TypeScript 型別確保資料正確 * 三種 UI 狀態:Loading / Error / Success * 只顯示前 10 筆:`.slice(0, 10)` --- ### ▶ API 資料結合 TodoList #### 在 `App.tsx` 引用 ```tsx import React, { useState, useEffect } from 'react'; import Hello from './components/Hello'; import HelloWorldButton from './components/HelloWorldButton'; import ChildButton from './components/ChildButton'; import TodoItem from './components/TodoItem'; import TodoInput from './components/TodoInput'; import type { Todo } from './components/TodoItem'; function App() { // ===== TodoList 狀態區 ===== const [todos, setTodos] = useState<Todo[]>([]); const [loading, setLoading] = useState(false); // Step 1: useEffect 抓 API 初始資料 useEffect(() => { setLoading(true); fetch('https://jsonplaceholder.typicode.com/todos') .then(res => res.json()) .then((data: any[]) => setTodos( data.slice(0, 10).map(todo => ({ id: todo.id, text: todo.title, done: todo.completed })) )) .finally(() => setLoading(false)); }, []); // Step 2: 本地新增 Todo const addTodo = (text: string) => setTodos([...todos, { id: Date.now(), text, done: false }]); // Step 3: 切換完成 const toggleTodo = (id: number) => setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t)); // Step 4: 刪除 Todo const deleteTodo = (id: number) => setTodos(todos.filter(t => t.id !== id)); // ===== 父子互動計數器組 ===== const [count, setCount] = useState<number>(0); const handleChildClick = () => setCount(count + 1); return ( <> <div> <h2>第一個元件</h2> <Hello name="宸維(Shiki)" age={21} /> </div> <br /> <div> <h2>第二個元件</h2> <HelloWorldButton /> </div> <br /> <div> <h2>第三個元件(父子元件互動練習)</h2> <p>目前點擊次數:{count}</p> <ChildButton onButtonClick={handleChildClick} /> </div> <br /> <div> <h2>Todo List (API 初始資料)</h2> {loading ? <p>載入中...</p> : ( <> <TodoInput onAdd={addTodo} /> <ul> {todos.map(t => ( <TodoItem key={t.id} todo={t} onToggle={toggleTodo} onDelete={deleteTodo} /> ))} </ul> </> )} </div> </> ); } export default App; ``` ![image](https://hackmd.io/_uploads/SyxZjmHNlx.png) ---