## 2025/9/05 ### 📘 React 第 2 堂 — Hook 入門 & UI 進階 官方對應章節:《Describing the UI》(後半)+《Adding Interactivity》(開頭) #### 1. Hook 基礎 **什麼是 Hook?** - React 元件就是一個 function,每次畫面更新時會「重新執行」。 - 如果只用普通變數,function 每次重播都會「失憶」,之前的值會消失。 - **Hook(例如 useState)就是 React 提供的工具,讓元件能記住狀態。** **Hook 的兩個限制** - 只能在元件最外層呼叫 - React 是靠「呼叫順序」來記錄狀態。 - 如果把 Hook 放在 if / for 裡,順序會亂掉,React 就會搞混。 - 只能在 React 元件或其他 Hook 裡呼叫 - Hook 只能存在於 React 管理的 function 中,這樣 React 才能保存和更新狀態。 **範例** ```jsx // ❌ 錯誤:放在 if 裡 function BadCounter({ show }) { if (show) { const [count, setCount] = useState(0); } return <p>這樣會壞掉</p>; } // ✅ 正確:放在最外層 function GoodCounter({ show }) { const [count, setCount] = useState(0); if (!show) return null; return <button onClick={() => setCount(c => c + 1)}>+1 ({count})</button>; } ``` **舉例說明** `useState` 會幫你建立一個「狀態值」以及「更新這個狀態的方法」。 常見的寫法是: ```jsx const [count, setCount] = useState(0); ``` 1. `useState(0)` - `useState` 是 React 的 **Hook**,用來在函式元件裡建立「狀態 (state)」。 - 這裡的 `0` 是「初始值」。 - 所以一開始 `sharedLikes = 0`。 2. 解構賦值 `[count, setCount]` `useState` 會回傳一個 **陣列**,裡面有兩個東西: 1. **目前的狀態值** → 這裡叫 `count` 2. **更新狀態的函式** → 這裡叫 `setCount` 所以你用陣列解構,把它們取名出來。 等價於: ```jsx const stateArray = useState(0); const count = stateArray[0]; // 當前狀態值 const setCount = stateArray[1]; // 更新狀態的函式 ``` 3. 怎麼用? - **讀取狀態** → 用 `Count` - **更新狀態** → 呼叫 `setCount` 4. 和一般 js 的count 差別? - **js** → 要手動更新 DOM `Count`才會更新 - **react** - React 會更新 count 的值。 - React 會觸發「元件重新渲染」,畫面才會顯示最新的數字 --- #### 2. useState 進階:快照陷阱 & 函式型更新 **快照陷阱** - React 的狀態更新不是立即的,而是「排隊等下次一併處理」。 - 多次 `setCount(count + 1)` 會用到「同一個舊值」,結果錯誤。 ```jsx // ❌ 快照陷阱:只會 +1 function CounterSnapshot() { const [count, setCount] = useState(0); const handleAddThree = () => { setCount(count + 1); setCount(count + 1); setCount(count + 1); }; return <button onClick={handleAddThree}>+3(陷阱): {count}</button>; } ``` **函式型更新(正確解法)** - 使用 `setCount(c => c + 1)`,保證每次都用最新的值。 ```jsx // ✅ 修正版:函式型更新 function CounterFixed() { const [count, setCount] = useState(0); const handleAddThree = () => { setCount(c => c + 1); setCount(c => c + 1); setCount(c => c + 1); }; return <button onClick={handleAddThree}>+3(正確): {count}</button>; } ``` **重點** - React 更新是非同步批次處理。 - 需要依賴前一個值時,務必使用「函式型更新」。 --- #### 3. 狀態隔離 vs 狀態共享 **狀態隔離** - 每個元件自己有自己的 state。 - 彼此之間互不干擾。 ```jsx function MyCounter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>+1 ({count})</button>; } function App() { return ( <><MyCounter /> <MyCounter /> <MyCounter /> </> ); } ``` **狀態共享** - 把 state 提升到父元件集中管理。 - 子元件透過 props 使用相同的 state。 ```jsx function MyButton({ count, onClick }) { return <button onClick={onClick}>+1 ({count})</button>; } function App() { const [count, setCount] = useState(0); return ( <><MyButton count={count} onClick={() => setCount(c => c + 1)} /> <MyButton count={count} onClick={() => setCount(c => c + 1)} /> </> ); } ``` **重點** - 狀態隔離:元件各自有獨立資料。 - 狀態共享:父元件集中管理,子元件同步更新。 :test_tube: 小測驗 - 兩種情況的重置如何設置? --- #### 4. 條件式 Rendering **概念** - React 的判斷是「要不要產生元素」。 - 與傳統 `display:none` 不同,React 元素未被渲染就不會存在於 DOM。 **三種常用寫法** ```jsx // if 語句 if (isLoggedIn) { return <p>歡迎回來!</p>; } return <p>請先登入</p>; // 三元運算子 ? : <p>{isLoggedIn ? "歡迎回來!" : "請先登入"}</p> // && 短路 <div> <p>首頁</p> {isLoading && <p>載入中...</p>} </div> ``` **(1) if 語句** 語法: ```jsx if (條件) { return <元素A />; } else { return <元素B />; } ``` - `if` 是 **語句 (statement)**,不能直接寫在 JSX 中。 - 通常放在 `return` 之前,決定整個 Component 要渲染什麼。 --- **(2) 三元運算子 ? :** 語法: ```jsx {條件 ? <元素A /> : <元素B />} ``` - 有三個部分:**條件 ? true時的值 : false時的值**。 - 是 **運算式 (expression)**,所以可以直接寫在 JSX 裡。 - 常用來處理「二選一」的情況。 --- **(3) && 短路運算** 語法: ```jsx {條件 && <元素A />} ``` - 如果 `條件 = true` → 顯示 `<元素A />`。 - 如果 `條件 = false` → React 忽略,不渲染任何東西。 - 適合「有就顯示、沒有就不顯示」。 --- **差異重點** - **if** → 適合整段邏輯判斷(例如:載入中就直接 return,後面不渲染)。 - **`? :` 三元運算子** → 適合小範圍「二選一」。 - **`&&` 短路運算** → 適合「有就顯示,沒有就不顯示」。 :test_tube: 小測驗 1. 如果 isLoggedIn = false,上面這一行會顯示什麼? 2. 如果 isLoading = true,整個畫面會怎樣? --- #### 5. 列表渲染進階 **為什麼需要 key?** - React 會比對舊陣列與新陣列,決定哪些需要更新。 - **key 是判斷元素身份的依據**,必須唯一且穩定。 - key 不正確 → React 搞錯元素 → 畫面錯位或輸入內容消失。 **錯誤示範:用 index 當 key** ```jsx {todos.map((todo, i) => <li key={i}>{todo}</li>)} ``` **正確示範:用唯一 id** ```jsx {todos.map(todo => <li key={todo.id}>{todo.text}</li>)} ``` **搭配排序與過濾** ```jsx <ul> {todos .filter(todo => !todo.done) .sort((a, b) => a.text.localeCompare(b.text)) .map(todo => <li key={todo.id}>{todo.text}</li>)} </ul> ``` **React 相對於 JS 的優勢** - 表單驗證:只新增錯誤提示,其他輸入框保持不變。 - 即時搜尋:只刪除不符合的項目,其他輸入框內容保留。 - 互動式表格:排序或刪除時,只更新有變動的 row,checkbox 與輸入值保持原狀。 **重點** - key 是元素的「身分證字號」,必須唯一且穩定。 - React 只更新差異,不會整個清空再重建。 :test_tube: 小測驗 1. 為什麼 key 不能用 index 當 key? --- #### 6. 回應 Event(Responding to Events) **概念** - 在 React,可以在 JSX 中綁定 **事件處理函式 (event handler)**。 - Event handler 會在互動發生時觸發(例如:點擊、輸入、hover)。 - event handler 本質上就是 **function**,通常命名為 `handleXxx`。 --- **基本範例** ```jsx export default function Button() { function handleClick() { alert("You clicked me!"); } return ( <button onClick={handleClick}> Click me </button> ); } ``` **重點** - `onClick={handleClick}` → 傳遞函式,而不是呼叫它。 - React 會在點擊時才執行 `handleClick`。 --- **Inline handler 寫法** ```jsx <button onClick={() => alert("You clicked me!")}> Click me </button> ``` - 適合簡短的邏輯。 - 本質上是傳遞一個匿名函式。 --- **常見陷阱** ```jsx // ❌ 錯誤:會在 render 時立即執行 <button onClick={handleClick()}>Click me</button> // ✅ 正確:傳遞函式 <button onClick={handleClick}>Click me</button> ``` --- **使用 props 的 event handler** ```jsx function AlertButton({ message, children }) { return ( <button onClick={() => alert(message)}> {children} </button> ); } export default function Toolbar() { return ( <div> <AlertButton message="Playing!">Play Movie</AlertButton> <AlertButton message="Uploading!">Upload Image</AlertButton> </div> ); } ``` - event handler 可以使用 component 的 props。 - 同一個元件可以因為不同 props 而有不同行為。 --- **父子傳遞 event handler** ```jsx function Button({ onClick, children }) { return <button onClick={onClick}>{children}</button>; } export default function Toolbar() { return ( <div> <Button onClick={() => alert("Playing!")}>Play Movie</Button> <Button onClick={() => alert("Uploading!")}>Upload Image</Button> </div> ); } ``` - parent 可以把邏輯傳下去,child 只負責顯示樣式與結構。 --- **事件傳遞與停止** ```jsx function Button({ onClick, children }) { return ( <buttononClick={(e) => { e.stopPropagation(); // 阻止冒泡 onClick(); }} > {children} </button> ); } export default function Toolbar() { return ( <divclassName="Toolbar" onClick={() => alert("You clicked on the toolbar!")} > <Button onClick={() => alert("Playing!")}>Play Movie</Button> <Button onClick={() => alert("Uploading!")}>Upload Image</Button> </div> ); } ``` - 點擊按鈕 → 只觸發按鈕自己的事件,不會冒泡到外層 `<div>`。 - `e.stopPropagation()` 阻止事件傳遞。 - `e.preventDefault()` 則是阻止瀏覽器的預設行為(例如表單送出)。 --- **重點整理** - **event handler 要傳遞函式,而不是呼叫**。 - 可以寫在外部,也可以 inline。 - 可以存取 props,讓相同元件行為不同。 - 可以從 parent 把事件處理邏輯傳給 child。 - 可以用 `stopPropagation` 或 `preventDefault` 控制事件傳遞與預設行為。 --- 🧪 小測驗 1. 下列哪個寫法是錯誤的? ```jsx <button onClick={handleClick}>Click</button> <button onClick={() => handleClick()}>Click</button> <button onClick={handleClick()}>Click</button> ``` 2. 點擊以下程式碼中的「Play Movie」按鈕會出現幾個 alert? ```jsx <div onClick={() => alert("Toolbar clicked!")}> <button onClick={() => alert("Playing!")}>Play Movie</button> </div> ``` 3. 要如何阻止表單送出時自動刷新頁面? --- #### 7. 小專案 Demo:SurveyForm 問卷填報系統 **功能** - 新增 / 刪除 回覆 - 搜尋 / 篩選 回覆 - 排序 (依名字升冪 / 降冪) - 受控表單驗證(姓名必填) **程式碼** (沿用 TodoListV2,不再重貼) --- #### 8. 課堂總結 - Hook 是 React 記住狀態的方式 - useState 的陷阱與進階用法 - 狀態隔離 vs 狀態共享 - 條件式 Rendering 的三種寫法 - 列表渲染與 key 的重要性 - 受控元件處理表單輸入 - 綜合練習:TodoList v2