# 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 元件庫,加速你的開發流程。
繼續保持學習的熱情,你將在前端開發的道路上越走越遠!