# React 從零到一:兩週學習計畫 歡迎來到 React 的世界!本計畫專為初學者設計,旨在透過為期兩週、每天約兩小時的學習,幫助你掌握 React 開發的核心技能。 **學習路徑:** * **第一週:React 核心基礎** - 我們將專注於 React 的基本概念,包括 JSX、元件、Props、State 以及事件處理。 * **第二週:進階概念與實戰** - 我們將學習 Hooks 的進階用法、路由、全域狀態管理,並最終完成一個小型實踐專案。 --- ## 第一週:React 核心基礎 ### Day 1: React 簡介與環境設定 **今日目標:** 了解 React 是什麼,以及如何設定你的第一個 React 開發環境。 #### 1. 什麼是 React? React 是一個由 Facebook 開發的開源 JavaScript UI 函式庫。它的主要特點是: * **元件化 (Component-Based):** 將複雜的 UI 拆分成一個個獨立、可重複使用的元件,讓程式碼更容易管理和維護。 * **宣告式 (Declarative):** 你只需要告訴 React 你希望 UI 長什麼樣子,React 會自動處理畫面的更新。 * **虛擬 DOM (Virtual DOM):** React 在記憶體中維護一個輕量級的 DOM 副本。當狀態改變時,它會先計算出新舊虛擬 DOM 的差異,然後才以最高效率的方式更新真實的 DOM,從而提升應用程式的效能。 #### 2. 環境設定 開始之前,你需要安裝 Node.js 和 npm(Node.js 的套件管理器)。 1. 前往 [Node.js 官網](https://nodejs.org/) 下載並安裝 LTS (長期支援) 版本。 2. 安裝完成後,打開你的終端機(Terminal 或 Command Prompt),輸入以下指令來確認是否安裝成功: ```bash node -v npm -v ``` 如果能看到版本號,代表安裝成功。 #### 3. 建立你的第一個 React 專案 我們使用官方推薦的工具 `Create React App` 來快速建立專案。 在你的終端機中,進入你想要建立專案的資料夾,然後執行: ```bash npx create-react-app my-first-react-app ``` 這個指令會建立一個名為 `my-first-react-app` 的資料夾,並在其中設定好所有必要的開發工具。 建立完成後,進入專案目錄並啟動開發伺服器: ```bash cd my-first-react-app npm start ``` 你的瀏覽器會自動打開 `http://localhost:3000`,並顯示 React 的歡迎頁面。 #### **練習 1** * **題目:** 成功建立一個新的 React 專案,並修改 `src/App.js` 檔案,將畫面中的預設內容替換成 "Hello, React!"。 * **提示:** 找到 `src/App.js` 中的 `<header>` 標籤,並將其內容替換掉。 #### **答案 1** 打開 `src/App.js`,將其內容修改如下: ```javascript import './App.css'; function App() { return ( <div className="App"> <h1>Hello, React!</h1> </div> ); } export default App; ``` 儲存檔案後,瀏覽器中的畫面會自動更新。 > #### **語法小教室:** > * `import './App.css';`:這是在引入 CSS 檔案,用來美化你的元件。`./` 表示檔案位於同一個資料夾。 > * `function App() { ... }`:這是在定義一個 JavaScript **函式 (Function)**,在 React 中,這種函式被稱為**函式元件**。`App` 是這個元件的名字。 > * `return ( ... );`:函式元件必須回傳一些東西,這些東西就是你想顯示在畫面上的內容。括號 `()` 是為了可以寫多行內容。 > * `<div>`, `<h1>`:這些看起來像 HTML 的標籤,其實是 **JSX**,我們明天會學到。`className` 對應到 HTML 的 `class` 屬性。 > * `export default App;`:這是在「匯出」這個 `App` 元件,這樣其他檔案(例如 `index.js`)才能「引入」並使用它。`default` 表示這是這個檔案最主要的匯出項目。 --- ### Day 2: JSX 語法與元件 (Components) **��日目標:** 學習 JSX 語法,並理解 React 中最重要的概念——元件。 #### 1. 什麼是 JSX? JSX (JavaScript XML) 是一個 JavaScript 的語法擴充。它讓你在 JavaScript 程式碼中可以像寫 HTML 一樣來描述 UI 結構。 **主要特點:** * 可以在 `{}` 中嵌入任何 JavaScript 表達式。 * HTML 屬性 `class` 在 JSX 中要寫成 `className`。 * 所有標籤都必須閉合,例如 `<img />` 或 `<div></div>`。 * 一個元件回傳的 JSX 必須被單一一個父層元素所包裹。 **範例:** ```javascript const name = "Alice"; const element = <h1>Hello, {name}</h1>; // 在 JSX 中使用變數 const user = { firstName: 'Bob', lastName: 'Smith' }; function formatName(user) { return user.firstName + ' ' + user.lastName; } const element2 = <h2>Welcome back, {formatName(user)}!</h2>; // 在 JSX 中使用函式 ``` > #### **語法小教室:** > * `const name = "Alice";`:`const` 是用來宣告一個**常數 (Constant)**,代表這個 `name` 變數在宣告後就不能再被改變。 > * `{ ... }`:在 JSX 中,大括號 `{}` 是一個「魔法通道」,可以讓你把 JavaScript 的變數、函式執行結果,甚至是簡單的計算(例如 `{1 + 1}`)放��去,React 會將其結果顯示在畫面上。 #### 2. 函式元件 (Functional Components) 在現代 React 中,我們主要使用函式元件。它就是一個回傳 JSX 的 JavaScript 函式。 **範例:** 在 `src` 資料夾下建立一個新檔案 `Greeting.js`: ```javascript // src/Greeting.js function Greeting() { return <h2>This is a greeting component.</h2>; } export default Greeting; ``` 然後在 `App.js` 中引入並使用它: ```javascript // src/App.js import './App.css'; import Greeting from './Greeting'; // 引入 Greeting 元件 function App() { return ( <div className="App"> <h1>Hello, React!</h1> <Greeting /> {/* 使用 Greeting 元件 */} </div> ); } export default App; ``` > #### **語法小教室:** > * `import Greeting from './Greeting';`:從 `./Greeting.js` 這個檔案引入它所匯出的 `Greeting` 元件。 > * `<Greeting />`:這就是使用一個 React 元件的方式,看起來就像一個自訂的 HTML 標籤。因為它沒有任何子內容,所以可以用 `/>` 來自我閉合。 #### **練習 2** * **題目:** 建立一個名為 `UserProfile.js` 的新元件。這個元件要顯示一個使用者的基本資料,包括姓名、���齡和一句自我介紹。在 `App.js` 中使用這個元件。 * **提示:** 在 `UserProfile.js` 中定義一些變數來儲存使用者資料,並在 JSX 中用 `{}` 來顯示它們。 #### **答案 2** 建立 `src/UserProfile.js`: ```javascript // src/UserProfile.js function UserProfile() { const user = { name: 'Charlie', age: 30, bio: 'I am a software developer and a React enthusiast.' }; return ( <div> <h3>User Profile</h3> <p><strong>Name:</strong> {user.name}</p> <p><strong>Age:</strong> {user.age}</p> <p><strong>Bio:</strong> {user.bio}</p> </div> ); } export default UserProfile; ``` 修改 `src/App.js` 來使用它: ```javascript // src/App.js import './App.css'; import UserProfile from './UserProfile'; function App() { return ( <div className="App"> <h1>My Application</h1> <UserProfile /> </div> ); } export default App; ``` --- ### Day 3: Props - 元件的屬性 **今日目標:** 學習如何使用 `props` 將資料從父元件傳遞到子元件。 #### 1. 什麼是 Props? `Props` (properties 的縮寫) 是元件的屬性。它是一個物件,允許你將資料從父元件傳遞到子元件,讓元件變得可重複使用且更具動態性。Props 是唯讀的,子元件不應該直接修改接收到的 props。 #### 2. 如何傳遞與接收 Props 在父元件中,你可以像 HTML 屬性一樣將資料傳遞給子元件。在子元件中,可以透過函式的第一個參數來接收 `props` 物件。 **範例:** 修改 `Greeting.js`,讓它可以接收一個 `name` 的 prop。 ```javascript // src/Greeting.js function Greeting(props) { // props 是一個物件: { name: "value_from_parent" } return <h2>Hello, {props.name}!</h2>; } export default Greeting; ``` > #### **語法小教室:** > * `function Greeting(props) { ... }`:`props` 是函式元件的**參數 (Parameter)**。當父元件傳遞屬性給 `Greeting` 元件時,React 會將這些屬性打包成一個物件,並作為 `props` 參數傳入。 > * `{props.name}`:因為 `props` 是一個物件,例如 `{ name: "Alice" }`,所以我們可以用 `.` 來存取物件裡面的屬性值。 在 `App.js` 中傳遞 `name` prop 給 `Greeting` 元件。 ```javascript // src/App.js import './App.css'; import Greeting from './Greeting'; function App() { return ( <div className="App"> <Greeting name="Alice" /> <Greeting name="Bob" /> <Greeting name="Charlie" /> </div> ); } export default App; ``` #### **練習 3** * **題目:** 修改昨天的 `UserProfile` 元件,讓它接收 `user` 物件作為一個 prop,而不是在元件內部寫死。在 `App.js` 中建立兩個不同的使用者物件,並使用 `UserProfile` 元件兩次來顯示它們的資料。 * **提示:** 在 `App.js` 中定義 `user1` 和 `user2` 物件,然後像這樣傳遞:`<UserProfile user={user1} />`。 #### **答案 3** 修改 `src/UserProfile.js`: ```javascript // src/UserProfile.js function UserProfile(props) { // 從 props 中解構出 user const { user } = props; return ( <div style={{ border: '1px solid #ccc', margin: '10px', padding: '10px' }}> <h3>User Profile</h3> <p><strong>Name:</strong> {user.name}</p> <p><strong>Age:</strong> {user.age}</p> <p><strong>Bio:</strong> {user.bio}</p> </div> ); } export default UserProfile; ``` > #### **語法小教室:** > * `const { user } = props;`:這是一種稱為「**物件解構 (Object Destructuring)**」的語法糖。它相當於 `const user = props.user;`。這讓我們可以更方便地從物件中取出想要的屬性並存成變數。 > * `style={{...}}`:在 JSX 中設定行內樣式需要使用兩個大括號。外層的 `{}` 表示這是一個 JavaScript 表達式,內層的 `{}` 則是一個真正的 JavaScript 物件,用來定義 CSS 樣式。屬性名稱要用駝峰式寫法,例如 `border-color` 要寫成 `borderColor`。 修改 `src/App.js` 來使用它: ```javascript // src/App.js import './App.css'; import UserProfile from './UserProfile'; function App() { const user1 = { name: 'David', age: 25, bio: 'Loves hiking and photography.' }; const user2 = { name: 'Eva', age: 32, bio: 'Enjoys reading and playing the piano.' }; return ( <div className="App"> <h1>User Management</h1> <UserProfile user={user1} /> <UserProfile user={user2} /> </div> ); } export default App; ``` --- ### Day 4: State 與 useState Hook **今日目標:** 學習元件的 `state` 以及如何使用 `useState` Hook 來管理元件的內部狀態。 #### 1. 什麼是 State? 如果說 `props` 是從外部傳入的資料,那麼 `state` 就是���件內部自己管理的資料。當 `state` 改變時,React 會自動重新渲染 (re-render) 該元件,以反映最新的狀態。 #### 2. `useState` Hook `useState` 是 React 提供的一個 **Hook** (鉤子),讓我們可以在函式元件中使用 state。你可以把 Hook 想像成一個能「鉤」住 React 核心功能(如 state)的特殊函式。 * **引入:** `import { useState } from 'react';` * **使用:** `const [state, setState] = useState(initialState);` * `useState` 接收一個 `initialState` (初始狀態) 作為參數。 * 它回傳一個陣列,包含兩個元素: 1. `state`:目前的狀態值。 2. `setState`:一個用來更新此狀態的函式。 **範例:一個計數器** 建立一個新元件 `Counter.js`。 ```javascript // src/Counter.js import { useState } from 'react'; function Counter() { // 宣告一個名為 count 的 state,初始值為 0 const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> {/* 點擊按鈕時,呼叫 setCount 來更新 count 的值 */} <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } export default Counter; ``` > #### **語法小��室:** > * `import { useState } from 'react';`:從 `react` 套件中引入 `useState` 這個 Hook。這裡的 `{}` 表示我們是「具名引入」,只引入我們需要的 `useState` 部分。 > * `const [count, setCount] = useState(0);`:這是一種「**陣列解構 (Array Destructuring)**」語法。`useState(0)` 會回傳一個陣列,例如 `[0, function]`。我們把陣列的第一個元素賦值給 `count` 變數,第二個元素賦值給 `setCount` 變數。 > * `setCount(count + 1)`:這是在**呼叫 (Call)** `setCount` 函式,並傳入新的狀態值 `count + 1`。當這個函式被呼叫後,React 就會安排一次元件的重新渲染。 在 `App.js` 中使用它: ```javascript // src/App.js import './App.css'; import Counter from './Counter'; function App() { return ( <div className="App"> <h1>Counter Example</h1> <Counter /> </div> ); } export default App; ``` #### **練習 4** * **題目:** 建立一個 `LightSwitch.js` 元件。這個元件有一個按鈕,按鈕上顯示 "Turn On" 或 "Turn Off"。畫面上同時顯示 "The light is On" 或 "The light is Off"。點擊按鈕可以切換燈的開關狀態。 * **提示:** 使用 `useState` 來儲存一個布林值(`true` 或 `false`)來代表燈的狀態。 #### **答案 4** 建立 `src/LightSwitch.js`: ```javascript // src/LightSwitch.js import { useState } from 'react'; function LightSwitch() { // true 代表開,false 代表關 const [isOn, setIsOn] = useState(false); const toggleLight = () => { // 使用 setIsOn 來更新狀態,值為目前狀態的相反 setIsOn(!isOn); }; return ( <div> <p>The light is {isOn ? 'On' : 'Off'}</p> <button onClick={toggleLight}> Turn {isOn ? 'Off' : 'On'} </button> </div> ); } export default LightSwitch; ``` 在 `App.js` 中使用它。 --- ### Day 5: 處理事件 (Handling Events) **今日目標:** 學習如何在 React 中處理使用者事件,例如點擊、輸入等。 #### 1. 事件處理 在 React 中處理事件與在原生 DOM 中非常相似,但有幾個語法上的差異: * React 事件的命名採用駝峰式命名法 (camelCase),例如 `onclick` 變成 `onClick`。 * 你傳遞的是一個函式作為**事件處理器 (Event Handler)**,而不是一個字串。 **範例:** ```javascript // ��遞一個函式 <button onClick={handleClick}>Click Me</button> // 而不是像 HTML 一樣傳遞字串 // <button onclick="handleClick()">Click Me</button> ``` #### 2. 處理表單輸入 對於表單元素如 `<input>`, `<textarea>`, `<select>`,我們通常使用 `onChange` 事件來監聽其值的變化,並將這個值存入 state。這種模式被稱為**受控元件 (Controlled Component)**。 **範例:一個簡單的輸入框** ```javascript // src/InputForm.js import { useState } from 'react'; function InputForm() { const [inputValue, setInputValue] = useState(''); const handleChange = (event) => { // event.target.value 包含了 input 的目前值 setInputValue(event.target.value); }; return ( <div> <p>Your input: {inputValue}</p> <input type="text" value={inputValue} onChange={handleChange} /> </div> ); } export default InputForm; ``` > #### **語法小教室:** > * `onChange={handleChange}`:`onChange` 是一個事件屬性,它會在輸入框內容改變時觸發。我們把 `handleChange` 這個函式本身(而不是函式的執行結果)傳給它。 > * `const handleChange = (event) => { ... }`:這���在定義一個**箭頭函式 (Arrow Function)**,它是 `function handleChange(event) { ... }` 的簡潔寫法。 > * `event`:當事件發生時,瀏覽器會產生一個**事件物件 (Event Object)**,並將它作為參數傳給我們的處理函式。這個物件包含了事件的相關資訊,例如 `event.target` 指向觸發事件的 DOM 元素(也就是那個 `<input>`),而 `event.target.value` 就是該元素的目前值。 在這個範例中,`<input>` 的值完全由 `inputValue` state 控制。每當使用者輸入時,`onChange` 事件觸發,`handleChange` 函式會更新 state,然後 React 重新渲染元件,將新的值顯示在 `<p>` 標籤和 `<input>` 的 `value` 中。 #### **練習 5** * **題目:** 建立一個 `SimpleLoginForm.js` 元件。這個表單包含一個 email 輸入框、一個 password 輸入框和一個提交按鈕。當使用者在輸入框中輸入時,不要做任何事。當使用者點擊提交按鈕時,用 `alert()` 顯示出 email 和 password 的值。 * **提示:** 你需要為 email 和 password 分別建立 state。在提交按鈕的 `onClick` 事件中處理顯示邏輯。 #### **答案 5** 建立 `src/SimpleLoginForm.js`: ```javascript // src/SimpleLoginForm.js import { useState } from 'react'; function SimpleLoginForm() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const handleSubmit = () => { alert(`Email: ${email}\nPassword: ${password}`); }; return ( <div> <h3>Login Form</h3> <div> <label>Email: </label> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} /> </div> <div style={{ marginTop: '10px' }}> <label>Password: </label> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} /> </div> <button style={{ marginTop: '10px' }} onClick={handleSubmit}> Submit </button> </div> ); } export default SimpleLoginForm; ``` > #### **語法小教室:** > * `onChange={(e) => setEmail(e.target.value)}`:這是一個**內聯 (inline) 的箭頭函式**。當 `onChange` 事件觸發時,這個小函式會被執行,它接收事件物件 `e`,然後呼叫 `setEmail` 並傳入 `e.target.value`。這種寫法很方便,當處理邏輯很簡單時,就��需要另外定義一個獨立的函式。 在 `App.js` 中使用它。 --- ### Day 6: 條件渲染 (Conditional Rendering) **今日目標:** 學習如何根據不同的條件來渲染不同的 UI。 #### 1. 條件渲染技術 在 React 中,你可以使用標準的 JavaScript 語法來進行條件渲染。常見的方法有: * `if...else` 陳述式 * **三元運算子 (Ternary Operator)**: `condition ? trueExpression : falseExpression` * **邏輯 `&&` 運算子**: `condition && expression` (如果條件為真,則渲染表達式) #### 2. 範例 **使用三元運算子** ```javascript // src/LoginStatus.js import { useState } from 'react'; function LoginStatus() { const [isLoggedIn, setIsLoggedIn] = useState(false); return ( <div> <h1>{isLoggedIn ? 'Welcome back!' : 'Please log in.'}</h1> <button onClick={() => setIsLoggedIn(!isLoggedIn)}> {isLoggedIn ? 'Logout' : 'Login'} </button> </div> ); } ``` > #### **語法小教室:** > * `isLoggedIn ? 'Welcome back!' : 'Please log in.'`:這就是**三元運算子**。它的意思是:如果 `isLoggedIn` 是 `true`,則表達式的值為 `'Welcome back!'`;否則(`false`),值為 `'Please log in.'`。它就像一個精簡版的 `if...else`。 **使用邏輯 `&&` 運算子** 當你只想在某個條件成立時才渲染某個元素,`&&` 就非常方便。 ```javascript // src/Mailbox.js function Mailbox(props) { const unreadMessages = props.unreadMessages; return ( <div> <h2>Hello!</h2> {unreadMessages.length > 0 && <h3> You have {unreadMessages.length} unread messages. </h3> } </div> ); } ``` > #### **語法小教室:** > * `condition && expression`:在 JavaScript 中,這個運算式的行為是:如果 `condition` 為 `true`,則回傳 `expression` 的值;如果 `condition` 為 `false`,則直接回傳 `condition` 的值(在 React 中,`false` 不會被渲染成任何東西)。所以這段程式碼的意思是:「如果 `unreadMessages` 陣列的長度大於 0,那麼就渲染 `<h3>` 這個元素。」 #### **練習 6** * **題目:** 建立一個 `LoadingSpinner.js` 元件。這個元件接收一個 `isLoading` 的 prop。如果 `isLoading` 為 `true`,則顯示 "Loading...";如果為 `false`,則顯示 "Data loaded successfully!"。 * **提示:** 使用三元運算子是最直接的方法。 #### **答案 6** 建立 `src/LoadingSpinner.js`: ```javascript // src/LoadingSpinner.js function LoadingSpinner(props) { const { isLoading } = props; return ( <div> {isLoading ? <p>Loading...</p> : <p>Data loaded successfully!</p>} </div> ); } export default LoadingSpinner; ``` 在 `App.js` 中使用它。 --- ### Day 7: 列表與 Keys **今日目標:** 學習如何渲染一個資料列表,並理解 `key` prop 的重要性。 #### 1. 渲染列表 我們通常使用 JavaScript 的 `.map()` 方法來將一個資料陣列轉換成一個 JSX 元素列表。 **範例:** ```javascript // src/TodoList.js function TodoList() { const todos = [ { id: 1, text: 'Learn React' }, { id: 2, text: 'Build a project' }, { id: 3, text: 'Go to bed' } ]; const todoItems = todos.map(todo => <li key={todo.id}> {todo.text} </li> ); return <ul>{todoItems}</ul>; } ``` > #### **語法小教室:** > * `todos.map(todo => ...)`:`.map()` 是 JavaScript 陣列的一個方法。它會遍歷陣列中的每一個元素,並對每個元素執行你提供的函式,最後將每次函式執行的回傳值組合成一個**新的陣列**。 > * 在這裡,`map` 函式遍歷 `todos` 陣列。對於每一個 `todo` 物件,��都回傳一個 `<li>` JSX 元素。最終,`todoItems` 會變成一個包含三個 `<li>` 元素的陣列,React 會將它們渲染出來。 #### 2. `key` Prop `key` 是一個特殊的字串屬性,當你在建立元素列表時需要包含它。 * **為什麼需要 `key`?** `key` 幫助 React 識別哪些項目被更改、新增或刪除。給予陣列中的元素一個穩定的 `key` 可以極大地提高列表渲染的效能。 * **如何選擇 `key`?** `key` 在其兄弟節點中必須是**唯一**的。最好的 `key` 是能唯一識別列表項目的 ID,例如資料庫中的 ID。**不建議使用陣列的索引 `index` 作為 `key`**,因為當列表順序改變時,這會導致效能問題和 state 的混亂。 #### **練習 7** * **題目:** 建立一個 `ProductList.js` 元件。這個元件接收一個 `products` 陣列作為 prop,`products` 陣列中每個物件包含 `id`, `name`, 和 `price`。元件需要將這個產品列表渲染到畫面上。 * **提示:** `products` 陣列的範例:`[{ id: 'p1', name: 'Apple', price: 1.2 }, { id: 'p2', name: 'Banana', price: 0.8 }]`。記得為每個列表項目設定唯一的 `key`。 #### **答案 7** 建立 `src/ProductList.js`: ```javascript // src/ProductList.js function ProductList(props) { const { products } = props; if (!products || products.length === 0) { return <p>No products available.</p>; } return ( <div> <h2>Product List</h2> <ul> {products.map(product => ( <li key={product.id}> {product.name} - ${product.price} </li> ))} </ul> </div> ); } export default ProductList; ``` 在 `App.js` 中使用它。 --- --- ## 第二週:進階概念與實戰 ### Day 8: `useEffect` Hook 與副作用 **今日目標:** 學習 `useEffect` Hook 來處理元件中的副作用,例如 API 請求或訂閱。 #### 1. 什麼是副作用 (Side Effects)? 在程式設計中,**副作用**指的是一個函式或表達式除了回傳值之外,還對外部世界產生了可觀察的影響。在 React 元件中,主要任務是根據 props 和 state 渲染 UI。任何超出這個範圍的操作都可以視為副作用。常見的副作用包括: * **資料獲取 (Fetching data from an API)**:與遠端伺服器溝通。 * **設定訂閱 (Setting up a subscription)**:監聽外部事件。 * **手動更改 DOM**:直接操作瀏覽器畫面(通常不建議)。 * **設定計時器 (setTimeout, setInterval)**:安排未來的程式碼執行��� #### 2. `useEffect` Hook `useEffect` 讓你在函式元件中執行副作用操作。它告訴 React:「在元件渲染到畫面上**之後**,請執行這個函式。」 * **語法:** `useEffect(setup, dependencies?)` * `setup`:一個包含副作用邏輯的函式。 * `dependencies` (可選):一個陣列,包含了 `setup` 函式所依賴的 props 或 state。 **`useEffect` 的執行時機 (由 `dependencies` 陣列決定):** 1. **沒有依賴項陣列:** `useEffect(() => { ... })` * 在**每次**元件渲染後都會執行(初始渲染和每次更新後)。這種情況較少使用,因為容易造成無限迴圈。 2. **空的依賴項陣列 `[]`:** `useEffect(() => { ... }, [])` * 只在元件第**一次**掛載 (mount) 到畫面上時執行一次。這是執行「只做一次」操作(如 API 請求)的理想選擇。 3. **有依賴項的陣列 `[dep1, dep2]`:** `useEffect(() => { ... }, [dep1, dep2])` * 在第一次掛載時執行,並且只有當陣列中的**任何一個**依賴項 (`dep1` 或 `dep2`) 的值發生變化時,才會再次執行。 #### 3. 清理副作用 有些副作用需要被清理,例如計時器或事件監聽,以避免**記憶體洩漏 (Memory Leak)**。`useEffect` 的 `setup` 函式可以回傳一個**清理函式 (cleanup function)**。這個函式會在元件卸載 (unmount) 前,或在下一次 `useEffect` 執行前被呼叫。 **範例:獲取假資料** ```javascript // src/DataFetcher.js import { useState, useEffect } from 'react'; function DataFetcher() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { // 這個 effect 只在元件初次渲染時執行一次 console.log('Component mounted, fetching data...'); fetch('https://jsonplaceholder.typicode.com/todos/1') .then(response => response.json()) .then(json => { setData(json); setLoading(false); }); }, []); // 空陣列表示沒有依賴 if (loading) { return <p>Loading...</p>; } return ( <div> <h2>Data from API</h2> <p>Title: {data.title}</p> </div> ); } export default DataFetcher; ``` > #### **語法小教室:** > * `useEffect(() => { ... }, []);`:`useEffect` 接收兩個參數:第一個是要執行的函式,第二個是依賴項陣列。 > * `fetch(...)`:這是瀏覽器內建的函式,用來向網路發送請求。它回傳一個 **Promise**,代表一個非同步操作的最終結果。 > * `.then(...)`:Promise 的方法。當 `fetch` 成功拿到回應後,第一個 `.then` 會被執行;當 `response.json()` 這個 Promise 解析完成後,第二個 `.then` 會被執行。這就是處理非同步操作的**鏈式呼叫**。 #### **練習 8** * **題目:** 建立一個 `Timer.js` 元件。這個元件在畫面掛載後,每秒鐘更新一次計數器並顯示在畫面上。當元件從畫面上移除時,必須清除計時器以避免記憶體洩漏。 * **提示:** 使用 `setInterval` 來設定計時器,並在 `useEffect` 的清理函式中用 `clearInterval` 來清除它。 #### **答案 8** 建立 `src/Timer.js`: ```javascript // src/Timer.js import { useState, useEffect } from 'react'; function Timer() { const [seconds, setSeconds] = useState(0); useEffect(() => { // 設定一個計時器 const intervalId = setInterval(() => { setSeconds(prevSeconds => prevSeconds + 1); }, 1000); // 清理函式 return () => { console.log('Clearing interval'); clearInterval(intervalId); }; }, []); // 空陣列確保 effect 和 cleanup 只執行一次 return ( <div> <h2>Timer: {seconds}s</h2> </div> ); } export default Timer; ``` > #### **語法小教室:** > * `setSeconds(prevSeconds => prevSeconds + 1)`:這是 `setState` 函式的一種**函式更新**形式。當新 state 需要依賴舊 state 計算時,傳入一個函式是更安全的做法。React 會將前一個 state (`prevSeconds`) 作為參數傳入,確保你拿到的是最新的值。 > * `return () => { ... }`:在 `useEffect` 的 `setup` 函式中 `return` 一個函式,這個回傳的函式就是**清理函式**。它會在元件要被銷毀時執行,讓我們有機會取消訂閱、清除計時器等。 --- ### Day 9: 自訂 Hooks (Custom Hooks) **今日目標:** 學習如何將可重複使用的邏輯抽離成自訂 Hooks。 #### 1. 為什麼需要自訂 Hooks? 當你在多個元件中發現自己寫了重複的邏輯(例如,同樣的 `useState` 和 `useEffect` 組合來獲取資料或監聽事件),這就是一個將它抽離成自訂 Hook 的好時機。自訂 Hook 的核心精神是**邏輯的複用**。 自訂 Hook 是一個以 `use` 開頭的 JavaScript 函式,它可以呼叫其他的 Hooks。這個命名慣例非常重要,它能讓 React 和開發者識別出這是一個 Hook。 #### 2. 建立一個自訂 Hook 讓我們���昨天獲取資料的邏輯抽離成一個 `useFetch` Hook。 建立 `src/hooks/useFetch.js` (建議將 hooks 放在獨立資料夾): ```javascript // src/hooks/useFetch.js import { useState, useEffect } from 'react'; function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { setLoading(true); fetch(url) .then(response => response.json()) .then(json => setData(json)) .catch(err => setError(err)) .finally(() => setLoading(false)); }, [url]); // 依賴 url,當 url 改變時重新獲取 return { data, loading, error }; } export default useFetch; ``` > #### **語法小教室:** > * `function useFetch(url) { ... }`:這就是一個自訂 Hook。它是一個普通的 JavaScript 函式,但因為它的名字以 `use` 開頭,並且內部呼叫了 `useState` 和 `useEffect`,所以它是一個 Hook。 > * `return { data, loading, error };`:這個 Hook 回傳一個物件,包含了它所管理的所有狀態。使用這個 Hook 的元件可以方便地解構出它們需要的資料。這是 `{ data: data, loading: loading, error: error }` 的簡寫。 #### 3. 使用自訂 Hook 現在,我們的元件可以變得非��簡潔。 ```javascript // src/UserData.js import useFetch from './hooks/useFetch'; function UserData({ userId }) { const { data, loading, error } = useFetch(`https://jsonplaceholder.typicode.com/users/${userId}`); if (loading) return <p>Loading user...</p>; if (error) return <p>Error fetching user!</p>; return ( <div> <h3>User Details</h3> <p>Name: {data.name}</p> <p>Email: {data.email}</p> </div> ); } export default UserData; ``` #### **練習 9** * **題目:** 建立一個 `useDocumentTitle` 的自訂 Hook。這個 Hook 接收一個 `title` 字串,並使用 `useEffect` 來更新瀏覽器分頁的標題 (`document.title`)。 * **提示:** `useEffect` 的依賴項應該是 `title`。 #### **答案 9** 建立 `src/hooks/useDocumentTitle.js`: ```javascript // src/hooks/useDocumentTitle.js import { useEffect } from 'react'; function useDocumentTitle(title) { useEffect(() => { document.title = title; }, [title]); // 當 title 改變時,更新文件標題 } export default useDocumentTitle; ``` 在任何元件中使用它: ```javascript // src/HomePage.js import { useState } from 'react'; import useDocumentTitle from './hooks/useDocumentTitle'; function HomePage() { const [count, setCount] = useState(0); // 使用自訂 Hook useDocumentTitle(`You clicked ${count} times`); return ( <div> <p>Check the document title in your browser tab!</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } export default HomePage; ``` --- ### Day 10: React Router **今日目標:** 學習使用 `React Router` 來建立擁有多個頁面的**單頁應用 (Single-Page Application, SPA)**。 SPA 的特點是整個網站只有一個 HTML 頁面,內容的切換是透過 JavaScript 動態改變畫面,而不是像傳統網站一樣每次都向伺服器請求一個新的 HTML 檔案。這使得使用者體驗更流暢、更像桌面應用程式。 #### 1. 安裝 React Router ```bash npm install react-router-dom ``` #### 2. 基本設定 在你的 `src/index.js` 或 `src/App.js` 中,用 `BrowserRouter` 包裹你的應用程式。 ```javascript // src/index.js import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <BrowserRouter> <App /> </BrowserRouter> </React.StrictMode> ); ``` > #### **語法小教室:** > * `<BrowserRouter>`:這是一個元件,它利用瀏覽器的 History API 來讓你的 UI 與 URL 保持同步。把它放在應用程式的最外層,所有路由相關的功能才能生效。 #### 3. 核心元件 * `<Routes>`: 包裹所有 `<Route>` 的容器。它會查看所有子 `<Route>`,並渲染第一個路徑匹配目前 URL 的那一個。 * `<Route>`: 定義一個路由規則。`path` 屬性定義 URL 路徑,`element` 屬性定義該路徑要渲染的元件。 * `<Link>`: 用來建立導覽連結,類似 HTML 的 `<a>` 標籤,但它會**避免頁面重新整理**,而是直接在內部改變 URL,觸發 `<Routes>` 重新匹配並渲染對應的元件。 #### 4. 範例 建立幾個頁面元件 `HomePage.js`, `AboutPage.js`。 在 `App.js` 中設定路由: ```javascript // src/App.js import { Routes, Route, Link } from 'react-router-dom'; import HomePage from './pages/HomePage'; import AboutPage from './pages/AboutPage'; function App() { return ( <div> <nav> <Link to="/">Home</Link> | {" "} <Link to="/about">About</Link> </nav> <hr /> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/about" element={<AboutPage />} /> </Routes> </div> ); } export default App; ``` #### **練習 10** * **題目:** 在你的應用中增加一個 `ContactPage`。並在導覽列中增加一個指向 `/contact` 的連結。 * **提示:** 建立 `ContactPage.js` 元件,並在 `App.js` 的 `Routes` 中增加一個新的 `Route`。 #### **答案 10** 建立 `src/pages/ContactPage.js`: ```javascript // src/pages/ContactPage.js const ContactPage = () => <h2>Contact Us</h2>; export default ContactPage; ``` 修改 `App.js`。 --- ### Day 11: Context API - 全域狀態管理 **今日目標:** 學習使用 React 的 Context API 來避免 "prop drilling" (屬性逐層傳遞)。 #### 1. 什麼是 Prop Drilling? 當一個深層的子元件需要來自高層父元件的資料時,你可能需要將 prop 一層一層地傳遞下去,即使中間的元件根本用不到這個 prop。這個過程就叫做 "prop drilling"。這會讓元件之間的耦合度變高,程式碼難以維護。 想像一下,你��把一個包裹從一樓送到五樓,但你必須親手交給二樓的人,二樓再交給三樓,三樓再交給四樓,最後才到五樓。Context API 就像是安裝了一部電梯,讓你可以從一樓直達五樓。 #### 2. Context API Context 提供了一種方法,可以讓資料在元件樹中直接傳遞,而無需手動傳遞 props。 **使用步驟:** 1. **建立 Context:** `const MyContext = React.createContext(defaultValue);` * 建立一個 Context 物件。`defaultValue` 是在沒有找到對應 Provider 時的備用值。 2. **提供 Context:** 使用 `MyContext.Provider` 包裹需要共享資料的元件樹,並透過 `value` prop 提供資料。 * 所有被 `Provider` 包裹的子元件都能存取到這個 `value`。 3. **消費 Context:** 在子元件中使用 `useContext(MyContext)` Hook 來讀取資料。 * 這是最常用、最簡單的消費方式。 #### 3. 範例:主題切換 (Theme Switcher) 建立 `src/context/ThemeContext.js`: ```javascript // src/context/ThemeContext.js import { createContext, useState }nfrom 'react'; // 1. 建立 Context export const ThemeContext = createContext(); // 建立一個 Provider 元件 (這是一個好習慣) export const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light')); }; return ( // 2. 提供 Context <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); }; ``` > #### **語法小教室:** > * `({ children })`:`children` 是一個特殊的 prop。它代表了被一個元件包裹的所有子元件。在這裡,`ThemeProvider` 會將它內部包裹的所有內容原封不動地渲染出來。 在 `App.js` 中使用 `ThemeProvider`: ```javascript // src/App.js import { ThemeProvider } from './context/ThemeContext'; import MyComponent from './MyComponent'; function App() { return ( <ThemeProvider> <MyComponent /> </ThemeProvider> ); } ``` 在任何子元件中消費 Context: ```javascript // src/MyComponent.js import { useContext } from 'react'; import { ThemeContext } from './context/ThemeContext'; function MyComponent() { // 3. 消費 Context const { theme, toggleTheme } = useContext(ThemeContext); // ... } ``` > #### **語法小教室:** > * `useContext(ThemeContext)`:這個 Hook 會向上尋找元件樹,找到最近的 `ThemeContext.Provider`,並回傳它的 `value` prop。如果找不到,則回傳 `createContext` 時設定的 `defaultValue`。 #### **練習 11** * **題目:** 建立一個 `AuthContext` 來管理使用者的登入狀態。這個 Context 應該提供 `isLoggedIn` (布林值) 和一個 `login`/`logout` 函式。建立一個 `LoginPage` 和一個 `ProfilePage`,`LoginPage` 顯示登入按鈕,`ProfilePage` 顯示 "Welcome!" 和登出按鈕。 * **提示:** 建立 `AuthContext.js`,並在 `App.js` 中使用 `AuthProvider`。根據 `isLoggedIn` 的狀態來決定顯示哪個頁面。 #### **答案 11** 建立 `src/context/AuthContext.js`: ```javascript // src/context/AuthContext.js import { createContext, useState, useContext } from 'react'; export const AuthContext = createContext(); export const AuthProvider = ({ children }) => { const [isLoggedIn, setIsLoggedIn] = useState(false); const login = () => setIsLoggedIn(true); const logout = () => setIsLoggedIn(false); return ( <AuthContext.Provider value={{ isLoggedIn, login, logout }}> {children} </AuthContext.Provider> ); }; // 建立一個自訂 Hook 來簡化 Context 的使用 export const useAuth = () => { return useContext(AuthContext); }; ``` 修改 `App.js`。 --- ### Day 12: 表單處理與驗證 **今日目標:** 學習更進階的表單處理技巧,並介紹��個流行的表單庫。 #### 1. 受控元件 (Recap) 我們之前學過受控元件,即表單的狀態由 React state 控制。對於複雜表單,這可能意味著需要管理很多 state。 #### 2. 處理複雜表單 當表單欄位變多時,可以將所有欄位存在一個物件 state 中。 ```javascript const [formData, setFormData] = useState({ username: '', email: '', password: '' }); const handleChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; ``` > #### **語法小教室:** > * `...prev`:這是**展開語法 (Spread Syntax)**。`{ ...prev }` 會建立一個 `prev` 物件的淺拷貝。 > * `[name]: value`:這是**計算屬性名稱 (Computed Property Names)**。它允許我們用一個變數(這裡的 `name`,值可能是 "username" 或 "email")來當作物件的屬性名稱。這使得我們可以只用一個 `handleChange` 函式來處理所有輸入框的變更。 #### 3. 介紹 `React Hook Form` 對於需要複雜驗證和狀態管理的表單,手動處理會變得很繁瑣。`React Hook Form` 是一個高效能、易於使用的表單庫,它將表單狀態的管理從 React state 中分離出來,減少了不必要的重新渲染,提升了效能。 **安裝:** ```bash npm install react-hook-form ``` **範例:** ```javascript // src/MyForm.js import { useForm } from 'react-hook-form'; function MyForm() { const { register, handleSubmit, formState: { errors } } = useForm(); const onSubmit = (data) => { console.log(data); alert('Form submitted successfully!'); }; return ( <form onSubmit={handleSubmit(onSubmit)}> {/* ... */} </form> ); } ``` > #### **語法小教室:** > * `useForm()`:`react-hook-form` 的核心 Hook,回傳一個包含多個有用方法和狀態的物件。 > * `register('username', { ... })`:這個函式用來「註冊」一個輸入框。它會回傳 `onChange`, `onBlur`, `name`, `ref` 等屬性,你需要用展開語法 `{...}` 將它們應用到 `<input>` 上。第二個參數是驗證規則物件。 > * `handleSubmit(onSubmit)`:這是一個高階函式。你把你的提交邏輯函式 (`onSubmit`) 傳給它,它會先觸發表單驗證,只有在驗證通過後,才會呼叫你的 `onSubmit`,並傳入表單資料 `data`。 > * `errors`:一個物件,包含了所有驗證失敗的欄位及其錯誤訊息。 #### **練習 12** * **題目:** 使用 `React Hook Form` 建立一個註冊表單,包含 `firstName` (必填) 和 `age` (必填,且必須是數字) 兩���欄位。 * **提示:** 使用 `register` 的第二個參數來設定驗證規則,例如 `required` 和 `valueAsNumber`。 #### **答案 12** 建立 `src/RegistrationForm.js`。 --- ### Day 13 & 14: 專案實戰 - Todo List App **目標:** 綜合運用前兩週所學的知識,建立一個功能完整的 Todo List 應用程式。 **功能需求:** 1. 顯示待辦事項列表。 2. 可以新增待辦事項。 3. 可以將待辦事項標記為已完成/未完成。 4. 可以刪除待辦事項。 5. (進階) 將待辦事項儲存在瀏覽器的 `localStorage` 中,這樣重新整理頁面後資料不會消失。 6. (進階) 可以篩選顯示「全部」、「已完成」、「未完成」的待辦事項。 > #### **語法小教室:`localStorage`** > * `window.localStorage` 是瀏覽器提供的一個儲存機制,可以讓你用鍵值對 (key-value pairs) 的形式儲存**字串**資料。 > * `localStorage.setItem(key, value)`:儲存資料。 > * `localStorage.getItem(key)`:讀取資料。 > * `JSON.stringify(object)`:將 JavaScript 物件或陣列轉換成 JSON 字串,以便儲存。 > * `JSON.parse(string)`:將 JSON 字串解析回 JavaScript 物件或陣列。 #### **專案結構建議** ``` src/ ├── components/ │ ├── TodoForm.js # 新增待辦事項的表單 │ ├── TodoList.js # 顯示列表 │ └── TodoItem.js # 單個待辦事項 ├── hooks/ │ └── useLocalStorage.js # (進階) 用於 localStorage 的自訂 Hook └── App.js # 主應用程式元件 ``` #### **Day 13: 核心功能開發** **練習 13:** 1. 在 `App.js` 中使用 `useState` 管理 `todos` 陣列。 2. 建立 `TodoForm.js`,包含一個輸入框和一個新增按鈕。當表單提交時,呼叫從 `App.js` 傳入的函式來新增一個 todo。 3. 建立 `TodoList.js`,接收 `todos` 陣列並使用 `.map()` 渲染出 `TodoItem` 元件列表。 4. 建立 `TodoItem.js`,顯示單個 todo 的內容,並包含一個「完成」按鈕和一個「刪除」按鈕。點擊按鈕時,呼叫從 `App.js` 傳入的對應函式。 #### **Day 14: 進階功能與優化** **練習 14:** 1. 建立 `useLocalStorage.js` 自訂 Hook,它和 `useState` 很像,但會自動將 state 同步到 `localStorage`。 2. 在 `App.js` 中用 `useLocalStorage` 替換 `useState` 來儲存 `todos`。 3. 增加篩選功能。在 `App.js` 中增加一個 state 來管理目前的篩選條件 (`'all'`, `'completed'`, `'active'`),並根據這個 state 來過濾要傳遞給 `TodoList.js` 的 `todos` 陣列。 --- #### **專案參考答案** (參考答案程式碼省略) --- ### 恭喜你! 你已經完成了為期兩週的 React 學習之旅!你從零開始,掌握了 React 的核心概念,並親手打造了一個完整的應用程式。 **接下來的學習方向:** * **TypeScript:** 為你的 React 專案增加型別安全。 * **狀態管理庫:** 學習 Redux Toolkit 或 Zustand 來管理更複雜的應用程式狀態。 * **React 框架:** 探索 Next.js (用於伺服器渲染和靜態網站生成) 或 Remix。 * **測試:** 學習使用 Jest 和 React Testing Library 來為你的元件編寫測試。 * **元件庫:** 熟悉 Material-UI (MUI) 或 Ant Design 等流行的 UI 元件庫,加速你的開發流程。 繼續保持學習的熱情,你將在前端開發的道路上越走越遠!