---
# System prepended metadata

title: 筆記｜第二堂：在 React 中操作 API
tags: [學習筆記, React]

---

`#React` `#六角前端課程` `#學習筆記` `#骨力走傱`
## JavaScript 同步與非同步
### 🔸 簡介
#### 同步語言
* 依序執行。
* 開發時會避免使用 `alert()`、`confirm()`、`prompt()` 這類的同步對話框，這會阻塞 JavaScript 的執行，導致使用者會無法與進行任何互動，這是不好的使用體驗；或者要等待 `for` 迴圈執行完大筆資料後，才能進行下一行程式碼，也是不好的使用體驗。

#### 非同步語言
* 不會依序執行，當有多行非同步語言時，無法確定哪一行會先執行。
* 若只有一行非同步語言的話，一定是最後執行。
* AJAX、`setTimeout()` ...

### 🔸 程式碼如何等待資料回傳？
#### Promise
```javascript=
const promiseSetTimeout = (status) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (status) {
        resolve('promiseSetTimeout 成功')
      } else {
        reject('promiseSetTimeout 失敗')
      }
    }, 0);
  })
};

promiseSetTimeout(1)
  .then((res) => {
    console.log(res);
    console.log(1);
    return promiseSetTimeout(1); // 使用 return 傳出結果
  })
  .then((res) => { // 再次使用 .then 接收
    console.log(res);
    console.log(2);
  })
    .catch((err) => {
    console.log(err);
  })
```
* 基於 Promise 建構的函式。
* `const promiseSetTimeout = (status) => { ... };` 傳入參數，決定結果是成功或失敗。
* `return new Promise((resolve, reject) => { ... };` 建構 Promise 的程式碼。
    * `resolve`：成功。
    * `reject`：失敗。
* 若直接執行 `promiseSetTimeout();` 會看不到結果，因為必須使用 `.then` 接收結果。
* 接收多個 Promise 的結果時，請避免使用巢狀結構接收，而是使用 `return` 傳出結果後，再使用 `.then` 接收。
* 若未使用 `.catch` 捕捉錯誤，會回傳 Uncaught (in promise) 的錯誤，表示未告知如何處理這個錯誤，接著會中斷同一區塊中（`{}`）程式碼的執行，因此需要設定捕捉錯誤的方法。

#### Async / Await
```javascript=
// 改寫前
const promiseSetTimeout = (status) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (status) {
        resolve('promiseSetTimeout 成功')
      } else {
        reject('promiseSetTimeout 失敗')
      }
    }, 10);
  })
};

// 改寫後
async function fn() {
    const data = await promiseSetTimeout(1); // await 必須接 Promise
    console.log(data);
};

// 箭頭函式
const fn = async () => {
    const data = await promiseSetTimeout(1);
    console.log(data);
};

// 立即函式
(async function fn() {
    const data = await promiseSetTimeout(1);
    console.log(data);
})();

// 捕捉錯誤
async function fn() {
    try{
        const data = await promiseSetTimeout(1);
        console.log(data);        
    } catch (error) {
        console.log(error);
    }
};

// 應用 axios
(async () => {
    try{    
        const res = await axios.get("...");
        const { results } = res.data;
        console.log(results);        
    } catch (error) {
        console.log(error);
    }
})();
```
* Async / Await 是 Promise 的語法糖，因此對 Promise 越熟悉，有助於使用 Async / Await。
* `async` 標記一個函式為「非同步函式」。
* `await` 運算子用於等待 Promise 的結果。它將非同步邏輯以同步語法呈現，取代了傳統的 `.then()` 鏈式調用，顯著提升代碼的可讀性。
* 注意 `async` 只有一個作用域 `{}`，故變數命名不能重複。
* 常見錯誤：
    * 忘記加 Async 或 Await。
    * Await 後方不是接 Promise，例如 `SetTimeout();`。
* 若在「等待 Promise 結果」且「沒有捕捉錯誤」的情況，會中斷同一區塊中（`{}`）程式碼的執行，因此需要設定捕捉錯誤的方法（`catch`）。

## API 與 RESTful API
### 🔸 簡介
> API（Application Programming Interface，應用程式介面）是一組定義不同軟體系統之間如何互動的規則和協定。
> 
> 簡單來說，**API 的主要目的是調用某些功能或服務，而不需要重新去寫這些功能。這使得軟體開發變得更加高效**，並且能夠利用其他系統或服務提供的功能。

> 舉例，你（使用者）去餐廳用餐，告訴服務員（API）你想吃什麼，接著服務生（API）會轉達給廚師（後台系統），廚師（後台系統）會幫你處理好菜色，再透過服務員（API）將餐點端給你（使用者）。—— [API串接功能，你不可不知道的網頁技術知識](https://www.lohaslife.cc/post/article-api-knowledge)

![VS---14’33”](https://hackmd.io/_uploads/SJkOtAB-Zg.jpg)

### 🔸 RESTful API 與請求方法
> RESTful API 是一種基於 HTTP 協議的 API 設計風格，通過簡單的 URL 和 HTTP 方法來操作資源。—— [新手指南：什麼是 RESTful API？](https://realnewbie.com/posts/what-is-restful-api)

> 其核心思想是，每一個 URL 都代表一個資源，而不同的 HTTP 方法（如 GET、POST、PUT、DELETE）用來對這些資源進行不同的操作。
> * `GET`　/users：取得用戶的資料。
> * `GET`　/users/{id}：取得特定用戶的資料。
> * `POST`　/users：新增新的用戶資料。
> * `PUT`　/users/{id}：修改特定用戶的資料。
> * `DELETE`　/users/{id}：刪除用戶資料。

### 🔸 如何運作？
![VS---56’05”](https://hackmd.io/_uploads/Hk7vDIJrWx.jpg)

* 瀏覽器中的網頁無法直接與資料庫連線，因此需要透過伺服器作為中介，負責資料的驗證、處理與傳遞。
* 發送請求（Request）一定是由前端（網頁）發請，後端（伺服器）不能發送請求。

### 🔸 課程 API
#### 文件
* [申請](https://ec-course-api.hexschool.io/)
* [課程 API 文件](https://hexschool.github.io/ec-courses-api-swaggerDoc/)

#### 路徑差異
* 後台管理者：/v2/api/{api_path}/admin/products/all
* 訪客：/v2/api/{api_path}/products/all

兩者差異在於路徑有沒有 **admin**，藉此區分使用者身分。

### 🔸 基本結構
![螢幕擷取畫面 (1706)](https://hackmd.io/_uploads/ryiU8wkHbl.png)
不是所有 API 需要夾帶參數，但驗證類的通常需要。

### 🔸 基本寫法
> 以客戶取得 `get` 產品為例。

```javascript=
const url = "https://ec-course-api.hexschool.io/";
const path = "***";

// 用樣板字面值組路徑
// console.log(`${url}/v2/api/${path}/products/all`)

const fn = async () => {
    try {
        const res = await axios.get(`${url}/v2/api/${path}/products/all`)
        const { products } = res.data;
        console.log(products);
    }
    catch (err) {
        console.log(err);
    }
};

fn();
```

## 登入驗證流程
### 🔸 基本流程
![VS---62’31”](https://hackmd.io/_uploads/SkAls8yrWx.jpg)

### 🔸 儲存 Token
#### 登入流程
```javascript=
function login() {
  const username = emailInput.value;
  const password = pwInput.value;
  const user = { // 文件要求的格式
    username,
    password
  }
  
  axios.post(`${url}/admin/signin`, user)
  .then((res) => {
    const { token, expired } = res.data; // 使用解構取出
    // 將 token 存在 cookie 的方法
    document.cookie = `hexToken=${ token }; expires=${ new Date(expired) }`;
  })
  .catch((err) => {
    console.dir(err);
  })
}
```
* 將帳號密碼送到登入 API。
* 從回傳資料中取得 `token` 與 `expired`。
* 將 `token` 存成 Cookie，並設定到期時間（後端設定）。

#### Cookie 寫法說明
```javascript=
// 基本範例
document.cookie = "someCookieName=true; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/";

// 使用情境
document.cookie = `hexToken=${token}; expires=${new Date(expired)}`;
```
* [Document.cookie](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/cookie#%E7%A4%BA%E4%BE%8B_2_%E5%BE%97%E5%88%B0%E5%90%8D%E4%B8%BA_test2_%E7%9A%84_cookie)
* 把登入用的 `token` 存入瀏覽器，讓後續請求可以驗證身分。

#### Cookie 組成拆解
`hexToken=${token}`
* `hexToken` 自訂的 Cookie 名稱。
* `token` 後端發給前端的身分驗證字串。
* 將 `token` 存成名為 `hexToken` 的 Cookie。

#### expires
`expires=${new Date(expired)}`
* `expires` Cookie 的有效期限。
* `expired` 後端提供的到期時間。
* 必須用 `new Date()` 轉換，瀏覽器才能正確解析。
    * `Date()` 回傳時間的「字串結果」。
    * `new Date()` 回傳 Date 物件，包含時間資料與操作方法。
        * 設定 Cookie 到期時間時，必須使用 `new Date()`。

#### uid 補充說明
* `uid`（User ID）。
* 唯一識別使用者。
* 區分不同帳號，用於追蹤使用者相關資料或行為。

### 🔸 取出與夾帶 Token
#### 驗證登入狀態
```javascript=
checkBtn.addEventListener('click', checkLogin);

// 取得 Token ( 僅需要設定一次 )
function checkLogin() {
  const token = document.cookie.replace(
    /(?:(?:^|.*;\s*)hexToken\s*\=\s*([^;]*).*$)|^.*$/,
    "$1"
  );

// 下次發送 axios 時 會自動將 token 夾帶至 headers 裡
  axios.defaults.headers.common['Authorization'] = token;

  // 確認是否登入
  axios.post(`${url}/api/user/check`)
    .then((res) => {
      console.log(res.data);
    })
    .catch((err) => {
      console.dir(err);
    });
}
```
* 從 Cookie 取出 `token`。
* 設定 Axios 預設 headers。
* 呼叫 API 確認登入狀態。

#### 取得 Cookie 中的 Token
```javascript=
const token = document.cookie.replace(
  /(?:(?:^|.*;\s*)hexToken\s*\=\s*([^;]*).*$)|^.*$/,
  "$1"
);

```
* `hexToken` Cookie 的名稱（key）。
* 正規表達式用途：
    * 從所有 Cookie 中。
    * 找出名為 `hexToken` 的值。
    * `$1`：代表被擷取到的 `token` 內容。
* 把存在 Cookie 裡的 `token` 取出來。

#### 將 Token 加入 Axios 預設標頭
`axios.defaults.headers.common['Authorization'] = token;`
* [Global axios defaults](https://github.com/axios/axios?tab=readme-ov-file#global-axios-defaults)
* 作用：
    * 設定 Axios 的全域預設請求標頭。
    * 之後所有 Axios 請求都會自動帶上 `Authorization: token`。
* 為什麼只需要設定一次：
    * `defaults` 是全域設定。
    * 不需要在每個 API 都手動加 headers。
* 實務建議：
    * 將這段放在「初始化階段」（例如程式一開始）。
    * 重新整理頁面後，只要 Cookie 還在，就能維持登入狀態。

#### 驗證是否登入
```javascript=
axios.post(`${url}/api/user/check`)
  .then((res) => {
    console.log(res.data);
  })
  .catch((err) => {
    console.dir(err);
  });
```
* 後端會從 `Authorization` 讀取 `token`。
* 驗證 `token` 是否有效。
* 回傳登入狀態結果。

#### 查看夾帶的 Token
![螢幕擷取畫面 (1707)](https://hackmd.io/_uploads/HkIwQOyrZg.png)

### 🔸 總結
> **登入成功後儲存 token → 初始化時取出 token → 發送請求時自動夾帶 token**

#### 1️⃣ 儲存 Token（登入成功後）
* 來源：後端回傳 `token`、`expired`。
* 動作：
  ```javascript
  document.cookie = `hexToken=${token}; expires=${new Date(expired)}`;
  ```
* 目的：將登入狀態「持久化」，讓重新整理頁面後仍能識別使用者。

#### 2️⃣ 取出 Token（初始化或需要驗證時）
* 來源：瀏覽器 Cookie。
* 動作：使用 `document.cookie + 正規表達式` 取得 `hexToken`。
* 目的：把瀏覽器中保存的 `token` 取回到 JavaScript 使用。

#### 3️⃣ 夾帶 Token（發送 API 請求）
* 動作：
  ```javascript
  axios.defaults.headers.common['Authorization'] = token;
  ```
* 目的：讓每次 API 請求都自動帶上身分驗證資訊，後端才能判斷使用者身分。

#### 關鍵觀念
:::warning
「取出 token」和「夾帶 token」**邏輯上是兩步，但實務上常寫在同一段初始化程式中**。
:::

## 其他方法
### 🔸 新增產品
```javascript=
axios.post(`${url}/api/${path}/admin/product`, product) // product 是提交的資料
  .then((res) => {
    console.log(res.data)
  })
  .catch((err) => {
    console.dir(err);
  })
```

### 🔸 刪除產品
```javascript=
axios.delete(`${url}/api/${path}/admin/product/{id}`) // 產品 id
  .then((res) => {
    console.log(res.data)
  })
  .catch((err) => {
    console.dir(err);
  })
```
:::warning
若要新增單筆資料，記得將路徑 products 修改成 product，複數反之。
:::

### 🔸 檔案上傳
```javascript=
const url = "https://ec-course-api.hexschool.io/v2"; // 請加入站點 Base URL
const path = ""; // 請加入個人 API Path

// 驗證
const token = document.cookie.replace(
  /(?:(?:^|.*;\s*)hexToken\s*\=\s*([^;]*).*$)|^.*$/,
  "$1");
axios.defaults.headers.common['Authorization'] = token;

const fileInput = document.querySelector('#file');
fileInput.addEventListener('change', upload);

function upload() {
  // 撰寫上傳的 API 事件
  const file = fileInput.files[0];
  const formData = new FormData();
  formData.append("file-to-upload", file); // 使用 append() 對應 "file-to-upload" 欄位、夾帶檔案 → 
  axios.post(`${url}/api/${path}/admin/upload`, formData)
    .then((res) => {
      console.log(res.data)
    })
    .catch((err) => {
      console.dir(err);
    })
}
```
* `const file = fileInput.files[0];`
    * 類陣列，使用 `[]` 取值。
* `new FormData();`
    * JS 用來產生表單格式的方法。
* `formData.append("file-to-upload", file);`
    * 使用 `append()` 對應 `"file-to-upload"` 欄位、夾帶檔案 → 第 15 行。

## Vite 安裝與環境建置環境
### 🔸 為什麼要使用 Vite？
> Vite 讓前端開發從「繁瑣設定」變成「直接開發」。

#### 1. 快速建立開發環境
* 不需從零設定環境。
* 透過預設選項即可立即開始開發。

#### 2. 適合開發單頁式應用（SPA）
* 快速建立前端應用服務。
* 支援現代前端框架（如 React、Vue）。

#### 3. 整合性高的開發工具
* 將多種開發流程整合在同一工具中。
* 降低環境設定與維護成本。

#### 4. 套件管理一致化
* 由 CDN 改為 NPM 管理套件。
* 依賴關係與版本更可控。

### 🔸 環境建立
![螢幕擷取畫面 (1708)](https://hackmd.io/_uploads/HJ5GOFkHWl.png)
```
npm create vite@latest
```
* Project name: （自定義專案名稱）
* Select a framework: › React
* Select a variant: › JavaScript

### 🔸 安裝 NPM
```
npm install
```

### 🔸 運行方式
```
npm run dev
```
* 運行開發環境。
* `dev` 並不是 npm 的指令，是透過 package.json 檔案新增指令的。

### 🔸 專案環境結構說明
#### 1. `node_modules`
* 存放專案所使用的第三方套件。
* 由套件管理工具自動產生，不需手動修改。

#### 2. `public`
* 不會被編譯。
* 存放靜態資源（如圖片、favicon）。
* 內容會直接以原始檔案形式提供給瀏覽器。

#### 3. `src`
* 會被編譯。
* 主要開發目錄，所有應用程式邏輯都在此。
* `main.jsx` 為專案進入點（entry point）。
  * 所有元件與程式碼必須從此檔案被引入，才會納入編譯流程。

#### 4. `eslint.config`
* 程式碼品質與風格檢查工具設定檔。
* 用於檢查錯誤、潛在問題與一致的撰寫規範。

#### 5. `.gitignore`
* 設定 Git 要忽略的檔案與資料夾。
* 通常包含不需版控的檔案（如 `node_modules`、環境設定檔）。

#### 6. `package.json`、`package-lock.json`
* 專案與套件的描述檔。
* 記錄：
  * 安裝的套件。
  * 套件版本。
  * 專案指令（scripts）。
* `package-lock.json` 用於鎖定實際安裝的套件版本。

#### 7. `vite.config`
* Vite 的設定檔。
* 用來設定：
  * 專案啟動方式。
  * 編譯與打包行為。
  * 編譯後輸出的調整方式。

### 🔸 專案部署流程（GitHub Pages）
#### 1. 將專案放上 GitHub
* 建立 GitHub Repository。
* 專案需先完成 `commit` 並 `push`。

#### 2. 透過 Git 指令操作部署流程
* 本地端以 git 管理版本與上傳程式碼。

#### 3. 建立正式版檔案
```
npm run build
```
* 產生可部署的靜態檔案。
* Vite 預設輸出至 `dist` 資料夾。

#### 4. 安裝 `gh-pages` 套件
```
npm install gh-pages --save-dev
```
* 用途：將靜態檔案一鍵部署至 GitHub Pages。
* 屬於開發工具，因此通常放在 `devDependencies`。

#### 5. 設定部署指令
在 `package.json` 的 `"scripts"` 中加入：
```
"deploy": "gh-pages -d dist"
```
* `gh-pages`：執行部署套件。
* `-d dist`：指定要部署的資料夾。
  * `-d` = **directory（目錄）**。

#### 6. 執行部署
```
npm run deploy
```
* 若成功，終端機會顯示 `Published`。
* 代表已成功部署到 GitHub Pages。

#### 總結
* `build`：產生靜態檔案。
* `gh-pages`：負責把 `dist` 推到 GitHub Pages。
* `deploy`：實際執行部署的指令。

### 🔸 調整靜態檔案路徑（Vite）
> GitHub Pages 不是從根目錄部署時，必須設定 `base`，否則靜態資源會找不到。
#### 問題與說明
* Vite 預設使用**絕對路徑**載入靜態資源。
* 部署到 **GitHub Pages（非根目錄）** 時，資源路徑會錯誤。
* 必須調整為對應 Repository 的路徑。

#### `vite.config.js` 設定
```javascript
export default defineConfig({
  base: process.env.NODE_ENV === "production" ? "/Repository名稱/" : "/",
  plugins: [react()],
})
```

#### 1. `base`
* 設定專案的基礎路徑（所有靜態資源的前綴）。

#### 2. 環境判斷
```javascript
process.env.NODE_ENV === "production"
```

* 判斷目前是否為正式環境：

| 指令              | 環境   | 路徑               |
| --------------- | ---- | ---------------- |
| `npm run dev`   | 開發環境 | `/`              |
| `npm run build` | 正式環境 | `/Repository名稱/` |

#### 為什麼這樣做？
* 本地開發仍維持 `/`，避免路徑錯誤。
* 部署後資源能正確從 GitHub Pages 的子路徑載入。

## 在 React 中操作 API
### 🔸 關注點分離下的事件處理觀念
> 不找 DOM，而是改狀態；DOM 只是結果。

```jsx=
<button type="button" id="login" onClick={() => login()}> // 把方法綁按鈕上
    登入
</button>
```
在強調關注點分離的框架（如 React）中，不直接操作或選取 DOM 元素，而是以「事件處理函式」取代，例如：
* `onClick`
* `onChange`
* `onSubmit`

事件發生時：
* 改變狀態（state）。
* 由狀態驅動畫面更新。

### 🔸 JavaScript vs React 的寫法
#### JavaScript
```javascript=
const emailInput = document.querySelector('#email');
const pwInput = document.querySelector('#password');

loginBtn.addEventListener('click', login);

function login() {
  const username = emailInput.value;
  const password = pwInput.value;
  const user = { 
    username,
    password
  }
  
  axios.post(`${url}/admin/signin`, user)
    .then((res) => {
      const { token, expired } = res.data; 
      document.cookie = `hexToken=${token}; expires=${new Date(expired)}`;
    })
    .catch((err) => {
      console.dir(err);
    })
}
```

#### React
```jsx=
// 建立 React 元件
function App() {
  
    // useState : 同時定義狀態與更新機制
  const [data, setData] = useState({
    username: "",
    password: "",
  });

    // 串接 API 與送出 data 給後端
  async function login() {
    try {
      const res = await axios.post(`${url}/admin/signin`, data)
    } catch (error) {
      console.log(error);
    }
  }

    // 單一事件處理器對應多個 input
  function eventHandler(e) {
    const { value, name } = e.target;
    setData({
      ...data,
      [name]: value,
    });
  }

    // React 會根據目前 state 把 JSX 轉成畫面
  return (
    <>
      <div>
        <input
          type="email"
          id="email"
          name="username"
          onChange={(e) => {
            eventHandler(e);
          }}
        />
        <input
          type="email"
          id="email"
          name="password"
          onChange={(e) => {
            eventHandler(e);
          }}
        />

        <button type="button" id="login" onClick={() => login()}>
          登入
        </button>
      </div>
    </>
  );
}
```

### 🔸 將畫面與資料抽離（關注點分離）
#### 核心概念
* 初學時，常把「畫面渲染」與「資料處理」寫在同一段程式碼，可先這樣寫沒關係，理解後再逐步抽離。
* 抽離的目的：
    * 降低重複程式碼。
    * 提高可讀性與可維護性。
    * 讓事件處理專心做「資料更新」。

#### 關注點分離前（畫面＋資料混在一起）
```jsx=
<input
  type="password"
  id="password"
  name="password"
  onChange={(e) => {
    const { value, name } = e.target;
    setData({
      ...data,
      [name]: value,
    });
  }}
/>
```
* `<input>` 本身就有 `name` 屬性，可作為資料欄位的 key。
* 透過解構，直接取得輸入值與欄位名稱。
    * `const { value, name } = e.target;`
* 使用動態屬性，根據不同 `input` 的 `name`，更新對應的 `state`（元件「目前的資料狀態」，React 知道後會重新渲染畫面）。
    * `[name]: value`
* 因此，同一段邏輯可處理多個 `input`。
    * （點擊帳號）`[name]: value` → `{ name } = e.target` → `name="username"`
    * （點擊密碼）`[name]: value` → `{ name } = e.target` → `name="password"`

#### 關注點分離後（抽離事件處理）
1️⃣ 抽出事件處理邏輯
```jsx=
{
const { value, name } = e.target;
setData({
  ...data,
  [name]: value,
});

```

2️⃣ 定義成 function
```jsx=
function eventHandler(e) {
  const { value, name } = e.target;
  setData({
    ...data,
    [name]: value,
  });
}

```

3️⃣ 在畫面中只負責「呼叫」
```jsx=
<input
  type="email"
  id="email"
  name="username"
  onChange={(e) => eventHandler(e)}
/>
```

### 🔸 setData：取代原本的 data
#### useState 更新是「覆蓋」
```jsx=
onChange={(e) => {
  const { value } = e.target;

  setData({
    ...data,        // 保留其他欄位
    username: value // 只更新這個欄位
  });
}}
```
* `setData` 會「整個取代 state」，不展開（複製一份）就會把其他欄位覆蓋。

#### 展開運算符在做什麼？
```jsx=
setData({
  ...data,
  username: value
});
```
等同於：
```jsx=
{
  password: ...,    // 複製一份 保留原始值
  remember: ...,    // 複製一份 保留原始值
  username: value   // 新值 覆蓋原始值
}
```
* 先把原本的 `data` 全部複製過來。
* 再覆蓋 `username`。

### 🔸 安裝 axios
```
npm install axios
```
```
import axios from 'axios';
```
* [axios](axios)
* 先暫停程式碼 `ctrl + c`
* 執行 `npm install axios`、接著引入 `import axios from 'axios';`
* 最後再啟動程式碼 `npm run dev`
* 可以在 `package-lock` 裡看到新增的套件

### 🔸 總結：React 的核心思維
#### 狀態管理
```
const [data, setData] = useState(...)
```
* `data`：狀態本身（目前的資料）。
* `setData`：更新這份狀態的唯一方法。
* `useState` 做的是：
    * 建立一份「狀態」。
    * 提供一個「觸發重新渲染的更新函式」。
* 不是只「定義更新方法」，而是同時定義狀態與更新機制。

#### 行為與副作用
```
function login()
```
* 負責「送出目前的 `data`」。
* 使用 `axios.post` 與後端 `/admin/signin` 溝通。
* 屬於行為（behavior）／副作用（side effect）。
    * 行為：使用者觸發後要「做的事」。
    * 副作用：會影響 React 以外世界的行為。

#### 事件與資料流
```
function eventHandler(e)
```
* 從 `e.target` 拿到：
    * `name`：決定要改哪個欄位。
    * `value`：新的值。
* 使用 `[name]: value` 動態更新對應欄位。
* 搭配 `...data` 保留其他欄位不被覆蓋。
* 這是單一事件處理器對應多個 `input` 的標準寫法。

#### 畫面渲染
```
return 之後的程式碼
```
* `return` 回傳的是 JSX。
* React 會根據目前 `state`，把 JSX 轉成畫面。
* 當 `setData` 被呼叫 → `state` 改變 → App 重新執行 → JSX 重新計算。