---
# System prepended metadata

title: Complete Intro to React

---

# Complete Intro to React

## React Hooks

### 具名/匿名 Component

匿名（箭頭函式）

```jsx
const Pizza = (props) => {
  return (
    <div
      className="pizza"
      onClick={() => {
        console.log("hi");
      }}
    >
      <h1>{props.name}</h1>
      <p>{props.description}</p>
      <img src={props.image} alt={props.name} />
    </div>
  );
};

or
const pizza = function (){
...
}

export default Pizza;

```

具名

```jsx
export default function Pizza () {
...
}
```

**💡 具名函式在有錯誤時比較好追蹤，推推**

### UseEffect

```jsx
1. 只執行一次時，放空陣列
2. useEffect 不要放 async 
   
useEffect(() => {
	fetchPizzaTypes()
}, [])
```

💡 Vue 3 中的 computed 屬性在 React 沒有，React 要自己追蹤變數

**💡 如果把 `useEffect` 的空陣列拿掉，高機率會發生無限迴圈，因為 fetchPizzaTypes 裡面有 setPizzaTypes，狀態改變會一直 re-render**

**💡 React 中正常用法是不會直接把 async 函式當作 `useEffect` 的參數。這是因為 `useEffect` 的回呼函式預期回傳的是「清除函式」或「無回傳」，而 `async` 函式會回傳 `Promise`，兩者不符合導致 React 無法正確處理。**

### Stric Mode

**核心概念：** 它是一個**「僅限開發模式 (development mode)」**的輔助工具。在生產環境 (production mode) 中會被自動移除，**完全不影響效能**。

**主要功能：**

1. **故意執行兩次 (Double Invocation)：**
    - **什麼：** 會故意重複執行你組件的 `render` 函式以及 `useEffect` (的掛載與卸載)。
    - **目的：** 為了抓出「不純的函式」(Impure Functions) 和「有副作用」(Side Effects) 且沒有正確清理的程式碼。
2. **警告「不安全」的語法：**
    - **什麼：** 當你用了 React 團隊不推薦、未來可能出問題的寫法時，會在 console 跳警告。
    - **範例：**
        - 不安全的生命週期方法 (如 `componentWillMount`)。
        - 即將過時的舊 API (如 string refs)。

### Dev tools

- 在 `console.log` 中輸入 `$r`, `$0` 可以 inspect 剛剛選取的元件、元素
- `useDeubugValue` 只有在 dev tool 出現

### useContext

### Props vs useContext

- **首選：Props Drilling (逐層傳遞)**
    - **優點：** 數據流明確、易維護。
    - **建議：** 雖然煩人，但盡量忍受。
    
- **`useContext`：謹慎使用**
    - **缺點：** 增加維護複雜性。
    - **使用時機：** 僅限於「**全域狀態 (App-Level State)**」。
    - **範例：** 使用者登入 (User)、網站主題 (Theme)、購物車 (Cart)。

### Example

1. `context.jsx` - 建立盒子，這只是一個「容器」或「盒子」，它本身不包含任何資料。
    
    ```jsx
    import { createContext } from "react";
    // createContext() 參數是預設值，因為要放 useState Hook 所以預設值給陣列 & 函式
    export const CartContext = createContext([[], function () {}]);
    ```
    
2. `App.jsx` - 使用 Provider 來「提供」value 給所有子組件
    
    ```jsx
    import { StrictMode, useState } from "react";
    import PizzaOfTheDay from "./PizzaOfTheDay";
    import Order from "./Order";
    import Header from "./Header";
    import { CartContext } from "./contexts";
    
    const App = () => {
      const cartHook = useState([]);
    
      return (
        <StrictMode>
          <CartContext.Provider value={cartHook}>
            <div>
              <Header />
              <Order />
              <PizzaOfTheDay />
            </div>
          </CartContext.Provider>
        </StrictMode>
      );
    };
    ```
    

- `Header.jsx` - 取值，React 會找到最近的那個 `Provider`，並回傳它的該 context 的值。
    
    ```jsx
    import { useContext } from "react";
    import { CartContext } from "./contexts";
    
    export default function Header() {
      const [cart] = useContext(CartContext);
      return (
        <nav>
          <h1 className="logo">Padre Gino's Pizza</h1>
          <div className="nav-cart">
            <span className="nav-cart-number">{cart.length}</span>
          </div>
        </nav>
      );
    }
    ```
    
### useReducer 相關觀念 QA
https://gemini.google.com/u/3/app/36da4cda920e3f68?pageId=none
## TanStack Router

## 建置 example

1. `vite.config.js`

```jsx
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: '<http://localhost:3000>',
        changeOrigin: true,
      },
      '/public': {
        target: '<http://localhost:3000>',
        changeOrigin: true,
      },
    },
  },
  plugins: [react(), TanStackRouterVite()],
})

```

1. `route/__root.jsx`

```jsx
import { useState } from "react";
import { createRootRoute, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import PizzaOfTheDay from "../PizzaOfTheDay";
import Header from "../Header";
import { CartContext } from "../contexts";

export const Route = createRootRoute({
  component: () => {
    const cartHook = useState([]);

    return (
    <>
      <CartContext.Provider value={cartHook}>
        <div>
          <Header />
          <Outlet />
          <PizzaOfTheDay />
        </div>
       </CartContext.Provider>
       
       <TanStackRouterDevtools />  
     </>
    );
  },
});

```

1. `App.jsx`

```jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";

const router = createRouter({ routeTree });

const App = () => {
  return (
    <StrictMode>
      <RouterProvider router={router} />
    </StrictMode>
  );
};

const container = document.getElementById("root");
const root = createRoot(container);
root.render(<App />);
```

💡 TanStack router 檔案命名範例參考

|命名規則|意義|是否 Lazy|
|---|---|---|
|`__root.jsx`|**不是一個實際的頁面路由，所有路由的共同父層**|❌ 否|
|`index.lazy.jsx`|延遲載入頁面，**會被渲染在 `__root.jsx` 的 `<Outlet />` 裡面**|✅ 是|
|`$id.lazy.jsx`|動態路由（lazy）|✅ 是|

## Component example

**`order.lazy.jsx`**

```jsx
import { useEffect, useState, useContext } from "react";
import { createLazyFileRoute } from "@tanstack/react-router";
import Pizza from "../Pizza";
import Cart from "../Cart";
import { CartContext } from "../contexts";

export const Route = createLazyFileRoute("/order")({
  component: Order,
});

function Order() {
  // ... 其他程式碼
}
```

### **鐵律:**

> 只要 `component` 檔案放在 `routes/` 資料夾,就必須寫 `createFileRoute` 或 `createLazyFileRoute`

### **檔案名稱: `order.lazy.jsx`**

→ 告訴 **TanStack Router CLI** 工具:「這個檔案要生成 lazy route」

### **程式碼: `createLazyFileRoute("/order")`**

→ 告訴 **TypeScript 編譯器**:「這個 component 對應的路徑是 **`/order`**」

這樣才能實現：

```jsx
router.navigate({ to: "/order" })  // ✅ 自動補全
router.navigate({ to: "/odrre" })  // ❌ TypeScript 報錯
```

### **但實際上缺點如下**

- ❌ **檔名和函數參數容易不一致** (例如檔名寫 **`order.lazy.jsx`** 但函數寫 **`/orders`**)
- ❌ **重複定義路徑** (檔名一次、函數一次)
- ❌ **開發體驗差** (就像你現在的感受)

## TanStack Query

> 課程概述
> 
> 有 TanStack Query 幾乎就不再用 useEffect
> 
> 加 ESLint Plugin & TanStack Query devtools

## **useQuery 是什麼？**

**用一句話說：它讓你不用再寫 `useState` + `useEffect` 來抓資料！**

### **傳統方式 vs useQuery**

**❌ 傳統方式 (麻煩)**

```jsx
function PizzaList() {
  const [pizzas, setPizzas] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    setIsLoading(true);
    fetch("/api/pizzas")
      .then(res => res.json())
      .then(data => {
        setPizzas(data);
        setIsLoading(false);
      })
      .catch(err => {
        setError(err);
        setIsLoading(false);
      });
  }, []);
  
  if (isLoading) return <div>載入中...</div>;
  if (error) return <div>錯誤: {error.message}</div>;
  return <div>{pizzas.map(p => <div>{p.name}</div>)}</div>;
}
```

**✅ 使用 useQuery (超簡單)**

```jsx
import { useQuery } from "@tanstack/react-query";

function PizzaList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ["pizzas"],
    queryFn: () => fetch("/api/pizzas").then(res => res.json()),
  });
  
  if (isLoading) return <div>載入中...</div>;
  if (error) return <div>錯誤: {error.message}</div>;
  return <div>{data.map(p => <div>{p.name}</div>)}</div>;
}
```

**帶參數的範例**

```jsx
function PizzaDetail({ pizzaId }) {
  const { data, isLoading } = useQuery({
	  // ⭐ 參數也要放進 queryKey
	  // ⭐ 告訴 TanStack Query：這是 pizza-{pizzaId} 的資料
    queryKey: ["pizza", pizzaId],  
    queryFn: () => fetch(`/api/pizzas/${pizzaId}`).then(r => r.json()),
  });

  if (isLoading) return <div>載入中...</div>;
  return <div>{data.name}</div>;
}
```

```jsx
// ❌ 錯誤做法
function PizzaDetail({ pizzaId }) {
  const { data } = useQuery({
    queryKey: ["pizza"],  // 所有披薩都用同一個 key
    queryFn: () => fetch(`/api/pizzas/${pizzaId}`).then(r => r.json()),
  });
}

// 使用：
<PizzaDetail pizzaId={1} />  // 抓取 ID=1，快取到 ["pizza"]
<PizzaDetail pizzaId={2} />  // ⚠️ 因為 queryKey 一樣，直接用快取的 ID=1 資料！

// 結果：ID=2 的披薩會顯示 ID=1 的內容！
```

### **核心概念**

- **queryKey - 資料的唯一識別碼**

```jsx
queryKey: ["pizzas"]           *// 抓取所有披薩*
queryKey: ["pizza", 1]         *// 抓取 ID=1 的披薩*
queryKey: ["pizzas", "spicy"]  *// 抓取辣味披薩*
```

**作用：**

- 識別這筆資料
- 自動快取（下次不用重新抓）
- 多個 component 共享同一筆資料

```jsx
// Component A
const { data } = useQuery({ queryKey: ["pizzas"], queryFn: fetchPizzas });

// Component B (不會重複發送請求，直接用快取！)
const { data } = useQuery({ queryKey: ["pizzas"], queryFn: fetchPizzas });

```

- **queryFn - 實際抓資料的函數**

```jsx
queryFn: () => fetch("/api/pizzas").then(res => res.json())
// **必須返回 Promise！**
```

- **queryKey 與 queryFn 的關係圖：**

```jsx
const { data } = useQuery({
  // 步驟 1: TanStack Query 看 queryKey
  queryKey: ["pizza", pizzaId],  
  // ↓
  // 「有這個 key 的快取嗎？」
  // - 有 → 直接返回快取資料
  // - 沒有 → 執行 queryFn
  
  // 步驟 2: 執行 queryFn 抓資料
  queryFn: () => fetch(`/api/pizzas/${pizzaId}`).then(r => r.json()),
  // ↓
  // 把結果存到 ["pizza", pizzaId] 這個快取位置
});

```

**重要觀念：queryKey 陣列的順序和值都要完全一樣**

```jsx
// ✅ 這兩個是「相同」的快取（會重用）
queryKey: ["pizzas", "margherita", "M"]
queryKey: ["pizzas", "margherita", "M"]

// ❌ 這兩個是「不同」的快取（順序不同）
queryKey: ["pizzas", "margherita", "M"]
queryKey: ["pizzas", "M", "margherita"]  // 順序不同！

```

**物件也可以放進 queryKey**

```jsx
// ✅ 這樣也可以（物件會被序列化）
queryKey: ["pizzas", { type: "margherita", size: "M" }]

// ✅ 物件屬性順序「不影響」（TanStack Query 會自動處理）
queryKey: ["pizzas", { type: "margherita", size: "M" }]
queryKey: ["pizzas", { size: "M", type: "margherita" }]  // 一樣的快取！
```

## useMutation
### 為什麼要用 `useMutation`？

雖然可以用 `try/catch` 自己寫 API 呼叫，但 `useMutation` 提供了標準化的狀態機：
1. **自動狀態變數**：直接給你 `isPending`, `isError`, `isSuccess` 用於 UI 切換，不用手寫一堆 `useState`。
2. **生命週期 Hook**：`onMutate` (送出前), `onSuccess` (成功), `onError` (失敗), `onSettled` (結束)，邏輯拆分清楚。
3. **進階功能**：支援 **Optimistic Updates (樂觀更新)**，在 API 回來前先偷改畫面，失敗再回滾，提升 UX。
### `useQuery` vs `useMutation`

核心差異：**意圖**與**觸發時機**。
#### 🔹 `useQuery` (讀取 / GET)

- **意圖**：向 Server 要資料
- **特性**：**有 Cache**、冪等 (Idempotent)
- **觸發**：**自動**。只要 Component Render 且 `enabled` 為 true，就會自動 fetch
- **Vue 類比**：`onMounted` 自動呼叫 API 並存入 `ref`

#### 🔸 `useMutation` (寫入 / POST, PUT, DELETE)

- **意圖**：改變 Server 狀態 (Side Effect)
- **特性**：**無 Cache**、一次性動作
- **觸發**：**手動**。必須呼叫 `.mutate()` 才會執行
- **Vue 類比**：API 呼叫 `method` ，綁在事件上

#### `useMutation` + `useQuery`  配合
`useMutation` 修改完資料後，`useQuery` 的 Cache 還是舊的。你需要手動建立連結：
修改成功 (`onSuccess`) ➝ 標記作廢 (`invalidateQueries`) ➝ 自動重抓 (Auto Refetch)
#### 完整 example
```jsx
import {
  useQuery,
  useMutation,
  useQueryClient,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
import { getTodos, postTodo } from '../my-api'

// Create a client
const queryClient = new QueryClient()

function App() {
  return (
    // Provide the client to your App
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  )
}

function Todos() {
  // Access the client
  const queryClient = useQueryClient()

  // Queries
  const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })

  // Mutations
  const mutation = useMutation({
    mutationFn: postTodo,
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })

  return (
    <div>
      <ul>
        {query.data?.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>

      <button
        onClick={() => {
          mutation.mutate({
            id: Date.now(),
            title: 'Do Laundry',
          })
        }}
      >
        Add Todo
      </button>
    </div>
  )
}

render(<App />, document.getElementById('root'))
```
## Advanced React Techniques
## createPortal

`createPortal` lets you render some children into a different part of the DOM. 
**對應 Vue 3 `<Teleport>`** 標籤

```jsx
import { createPortal } from 'react-dom';  

// ...  

<div>  
	<p>This child is placed in the parent div.</p>  
	
	{createPortal(  
	<p>This child is placed in the document body.</p>,  
	document.body  
	)}  
</div>
```
### Parameters

- `children`: Anything that can be rendered with React, such as a piece of JSX (e.g. `<div />` or `<SomeComponent />`), a [Fragment](https://react.dev/reference/react/Fragment) (`<>...</>`), a string or a number, or an array of these.

- `domNode`: Some DOM node, such as those returned by `document.getElementById()`. The node must already exist. Passing a different DOM node during an update will cause the portal content to be recreated.

- **optional** `key`: A unique string or number to be used as the portal’s [key.](https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key)


> [!NOTE] Caveat
> - Events from portals propagate according to the React tree rather than the DOM tree. For example, if you click inside a portal, and the portal is wrapped in `<div onClick>`, that `onClick` handler will fire. If this causes issues, either stop the event propagation from inside the portal, or move the portal itself up in the React tree.



## 課程範例(先 skip)  %% fold %%

### createPortal 

Portal 讓你把元件渲染到**父元件 DOM 樹以外**的地方,常用於 Modal、Toast、Tooltip 等需要脫離父元件 CSS 限制的場景 。

**對應 Vue**: Vue 3 的 **`<Teleport>`** 元件

**Modal 元件實作**

```jsx
import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";

const Modal = ({ children }) => {
  // 1. 建立 ref 來儲存 DOM 容器
  const elRef = useRef(null);
  
  // 2. 檢查並建立 DOM 元素 (只在第一次執行)
  if (!elRef.current) {
    elRef.current = document.createElement("div");
  }

  // 3. 掛載和清理 DOM
  useEffect(() => {
    const modalRoot = document.getElementById("modal");
    modalRoot.appendChild(elRef.current);
    
    // cleanup: 元件卸載時移除 DOM
    return () => modalRoot.removeChild(elRef.current);
  }, []);

  // 4. 使用 Portal 渲染內容
  return createPortal(<div>{children}</div>, elRef.current);
};

export default Modal;

```

```jsx
function App() {
  const [showModal, setShowModal] = useState(false);
  
  return (
    <>
      <button onClick={() => setShowModal(true)}>打開彈窗</button>
      
      {showModal && (
        <Modal>
          <h1>這是彈窗內容</h1>
          <button onClick={() => setShowModal(false)}>關閉</button>
        </Modal>
      )}
    </>
  );
}
```

#### 執行流程詳解  %% fold %%

第一次渲染 (元件掛載)

```jsx
1. 建立 ref 物件
   elRef = { current: null }

2. 檢查並建立空 div
   elRef.current = document.createElement("div")
   此時這個 div 是獨立的,還沒掛到頁面上

3. 執行 createPortal
   return createPortal(<div>{children}</div>, elRef.current)
   
   意思: 把 <div>{children}</div> 渲染到 elRef.current 這個 DOM 容器裡
   
   ⚠️ 注意: elRef.current 是「真實 DOM 元素」,不是 JSX!

4. useEffect 執行 (渲染完成後)
   找到 #modal 元素
   把 elRef.current (已包含渲染內容) 掛到 #modal 裡
```

最終的 DOM 結構

```jsx
<div id="root">
  <button>打開彈窗</button>
</div>

<div id="modal">
  <div>                    *<!-- elRef.current (createElement 建立的容器) -->*
    <div>                  *<!-- createPortal 渲染的內容 -->*
      <h1>這是彈窗內容</h1>
      <button>關閉</button>
    </div>
  </div>
</div>
```

元件卸載時

```jsx
1. React 呼叫 useEffect 的 cleanup function
2. 執行 modalRoot.removeChild(elRef.current)
3. 整個容器從 #modal 中移除,避免記憶體洩漏
```

<aside> 💡

React 的渲染流程 :

1. 執行元件函數 (虛擬 DOM 建立)
2. 比對差異
3. 更新真實 DOM
4. **DOM 準備完成後才執行 useEffect** </aside>
## useRef  

`useRef` lets you reference a value that’s not needed for rendering.

```jsx
import { useRef } from 'react';

const myRef = useRef(initialValue);
*// 回傳: { current: initialValue }*
```

### **核心特性**

- 改變 **`ref.current`** **不會觸發 re-render**
- 每次 render 都拿到**同一個物件**
- 值的更新是**同步且即時**的，不用等下次 render

### Caveat

> [!warning] **Do not write _or read_ `ref.current` during rendering.**

```jsx
function MyComponent() {  
// 🚩 Don't write a ref during rendering 
	myRef.current = 123; 
	
// 🚩 Don't read a ref during rendering 
	return <h1>{myOtherRef.current}</h1>;
}
```

You can read or write refs **from event handlers or effects instead**.

```jsx
function MyComponent() {  
	// ...  
	useEffect(() => {  
		// ✅ You can read or write refs in effects  
		myRef.current = 123;  
	});  
	
	// ...  
	function handleClick() {  
		// ✅ You can read or write refs in event handlers  
		doSomething(myOtherRef.current);  
	}  
	// ...  
}
```

If you _have to_ read [or write](https://react.dev/reference/react/useState#storing-information-from-previous-renders) something during rendering, [use state](https://react.dev/reference/react/useState) instead.
### 常用場景

+ 操作 DOM
+ 存取不影響畫面的資料

+ 避免 recreate ref content
```jsx
function Video() {  
	const playerRef = useRef(null);  
	if (playerRef.current === null) {  
	playerRef.current = new VideoPlayer();  
}  
// Normally, writing or reading `ref.current` during render is not allowed. However, it’s fine in this case because the result is always the same, and the condition only executes during initialization so it’s fully predictable.
```


> [!NOTE] 與 Vue ref 的關鍵差異
> **關鍵差異**: Vue 的 **`ref()`** 是**響應式系統的一部分**，React 的 **`useRef`** 是**脫離響應式系統**的工具 
> 
|**特性**|**React useRef**|**Vue ref**|
|---|---|---|
|存取方式|**`.current`**|**`.value`**|
|是否響應式|**否**，改變不觸發更新|**是**，改變會觸發更新|
|主要用途|DOM 存取、儲存非響應式資料|響應式資料 + DOM 存取|
|更新時機|同步即時|下次 tick|

## Class Component

> [!NOTE] 核心對照表
> | Function Component                     | Class Component                  | 說明       |
| -------------------------------------- | -------------------------------- | -------- |
| `const [state, setState] = useState()` | `this.state` + `this.setState()` | 管理狀態     |
| `useEffect(() => {}, [])`              | `componentDidMount()`            | 元件掛載後執行  |
| `useEffect(() => { return () => {} })` | `componentWillUnmount()`         | 元件卸載前執行  |
| `useEffect(() => {}, [dep])`           | `componentDidUpdate()`           | 依賴更新時執行  |
| `const value = props.name`             | `const value = this.props.name`  | 讀取 props |

### 範例對照
#### Function Component
```jsx
import { useState, useEffect } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    console.log('元件掛載了');
    return () => console.log('元件卸載了');
  }, []);
  
  const handleClick = () => {
    setCount(count + 1);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  );
}

```
#### Class Component (同樣功能)
```jsx
import React from 'react';

class Counter extends React.Component {
  constructor(props) {
    super(props);
    // state 在這裡定義
    this.state = {
      count: 0
    };
    // 綁定 this (重要！)
    this.handleClick = this.handleClick.bind(this);
  }
  
  // 元件掛載後執行
  componentDidMount() {
    console.log('元件掛載了');
  }
  
  // 元件卸載前執行
  componentWillUnmount() {
    console.log('元件卸載了');
  }
  
  // 事件處理函數
  handleClick() {
    // 更新 state
    this.setState({ count: this.state.count + 1 });
  }
  
  // 必須有 render 方法
  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>+1</button>
      </div>
    );
  }
}

export default Counter;

```
##### ==Class Component 重點說明==

##### 1. Constructor (建構函數)
```jsx
constructor(props) { 
	super(props); // 必須先呼叫,否則會報錯 
	this.state = { count: 0 }; // 初始化 state 
}
```
##### 2. this 的綁定問題
```jsx
// ❌ 錯誤:this 會是 undefined
<button onClick={this.handleClick}>

// ✅ 方法 1: 在 constructor 綁定
this.handleClick = this.handleClick.bind(this);

// ✅ 方法 2: 使用箭頭函數 (推薦)
handleClick = () => {
  this.setState({ count: this.state.count + 1 });
}

```

> [!NOTE] **為什麼要綁 this？**
> + 因為在 JavaScript 的 Class 中，你定義的 `handleClick()` 方法，預設並不會自動綁定 `this`
> + 當你把它（`this.handleClick`）當作「值」傳給 `onClick={...}` 之後，**未來**觸發事件時，React 呼叫它，它其實是在一個**錯誤的上下文 (Context)** 中被執行的，所以它內部的 `this` 會是 `undefined`（或 `window`），而不是你的 Component 實例。

> [!NOTE] **`bind` 和「賦值」做了什麼？**
> - `this.handleClick.bind(this)`：這會**建立一個「全新的函式」**。這個新函式跟原本的 `handleClick` 內容一模一樣，但差別在於它的 `this` **永遠被鎖定**在 `constructor` 當下指向的 `this`（也就是你的 Component 實例）。
>   
> - `this.handleClick = ...`：這是一個「**賦值**」動作。你把這個「新建立的、已綁定的函式」，_覆蓋_掉 Class 實例上原本那個「未綁定的 `handleClick`」。
>   
##### 3. 更新 State
```jsx
// ❌ 錯誤:不能直接修改
this.state.count = 1;

// ✅ 正確:使用 setState
this.setState({ count: 1 });

// ✅ 使用前一個 state
this.setState((prevState) => ({
  count: prevState.count + 1
}));
```
##### 4. 生命週期方法​
```jsx
class MyComponent extends React.Component {
  // 掛載階段
  componentDidMount() {
    // 元件第一次渲染後執行
    // 等於 useEffect(() => {}, [])
  }
  
  // 更新階段
  componentDidUpdate(prevProps, prevState) {
    // 元件更新後執行
    // 等於 useEffect(() => {}, [dependency])
    if (prevState.count !== this.state.count) {
      console.log('count 改變了');
    }
  }
  
  // 卸載階段
  componentWillUnmount() {
    // 元件移除前執行
    // 等於 useEffect cleanup function
  }
  
  render() {
    return <div>{this.state.count}</div>;
  }
}

```
### ErrorBoundaries
**ErrorBoundaries 是 React 的安全網，**捕捉「渲染（Rendering）過程」中發生的錯誤**，顯示 fallback 畫面而不是讓整個網站變白畫面**
> [!warning] 只有 Class Component 可以實作 Error Boundaries
> Function Component 都用 `react-error-boundary` 套件

## Testing

## 安裝套件
- **`vitest`**: 測試運行的引擎 (Test Runner)。
- **`@testing-library/react`**:用來 Render React 組件並進行操作的工具庫。
- **`happy-dom`**: 模擬瀏覽器環境的 DOM 實作（給 Node.js 用的）。

```
npm install -D vitest@2.1.3 @testing-library/react@16.0.1 happy-dom@15.7.4
```

```jsx
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    // 🔥 這裡！告訴 Vitest 使用 happy-dom 來模擬瀏覽器環境
    environment: 'happy-dom', 
  },
})
```

> [!NOTE] `-D` 是什麼意思？
> `-D` 其實是 `--save-dev` 的縮寫。
> 當你加上 `-D` 時，npm 會把這些套件寫入 `package.json` 中的 **`"devDependencies"`** 區塊，而不是一般的 `"dependencies"` 區塊。
> 什麼時候要加 `-D`？什麼時候不用？
> 
> 這個判斷標準其實非常簡單，只要問自己一個問題： **「當我的使用者在瀏覽器打開網頁（或 App 上線運行）時，需不需要這段程式碼？」**

## 純展示元件 example

```tsx
afterEach(cleanup)

test("alt test renders on Pizza inage", async () => (
	const name = "My Favorite Pizza";
	const src = "https://picsum.photos/200";
	const screen = render(
  <Pizza name={name} description="super cool pizza" image={src} />
);
	const img = screen.getByRole("img");
	expect(img.src).toBe(src);
	expect(img.alt).toBe(name);
});
```

這段 code 的邏輯就是標準的測試三部曲：**Arrange (準備) -> Act (執行) -> Assert (斷言/檢查)**。

#### 1. 準備假資料 (Arrange)
```jsx
const name = "My Favorite Pizza";
const src = "https://picsum.photos/200";
```

因為是在測試環境，沒有真的 API 資料，所以我們自己定義一些字串當作假資料 (Mock Data)。
#### 2. 渲染組件 (Act)
```jsx
const screen = render(
  <Pizza name={name} description="super cool pizza" image={src} />
);
```

- **`render()`**：這是 React Testing Library 的核心 function。它會在一個「虛擬的 DOM」中把你的 `<Pizza />` 組件畫出來。
- 你可以把它想像成：機器人在記憶體裡面偷偷開了一個瀏覽器頁面，把你的組件掛上去。
#### 3. 抓取元素 (Act / Query)
```jsx
const img = screen.getByRole("img");
```

- **`screen.getByRole("img")`**：這一步是在模擬使用者的行為。
- 它的意思是：「在剛剛畫出來的畫面中，幫我找出扮演『圖片 (img)』角色的那個 HTML 元素」。
- **`getByRole`**: 這是 React Testing Library 提倡的查詢方式。比起用 `querySelector('.my-class')`，用 `Role` 查詢更接近螢幕閱讀器 (Screen Reader) 的運作方式，能確保網頁親和力。

#### 4. 驗收結果 (Assert)
```jsx
expect(img.src).toBe(src);
expect(img.alt).toBe(name);
```

- **`expect(...).toBe(...)`**：這就是判官。
- 第一行檢查：剛剛抓到的那張圖片，它的 `src` 屬性網址，是不是我剛剛傳進去的那個網址？    
- 第二行檢查：那張圖片的 `alt` (替代文字) 屬性，是不是我剛剛傳進去的 `name`？

#### `cleanup` 的作用
它負責在每一個測試 (`test` 或 `it`) 跑完之後，把剛剛 `render` 出來的 React 組件從虛擬 DOM 上 **卸載 (Unmount)** 並清空。確保下一個測試進來時，面對的是一張乾淨的白紙。

---

#### 這段 Code 想要教會你什麼觀念？

除了基本的測試語法外，它隱含了兩個 React 開發的重要觀念：

1. **Props 的傳遞驗證**： 它在確保你的 Component 內部寫法是正確的。如果你的 `<Pizza>` 組件內部寫錯，例如忘記把 `props.image` 綁定到 `<img src={...}>` 上，這個測試就會失敗 (Fail)。這是用來防止開發者手殘改壞程式碼。

2. **重視無障礙網頁 (Accessibility / a11y)**： 注意它的測試標題叫 `"alt test renders on Pizza image"`，而且它特別檢查了 `img.alt`。 這是在教導開發者：**「圖片一定要有替代文字 (alt text)」**。如果你在開發 Component 時偷懶沒寫 `alt`，測試就會報錯。這是一種透過測試來強制執行 Coding Style 的手段。

## Mock Request Example（課程範例用 fetch not axios）

React 聯絡表單整合測試 (Integration Test)
**技術線：** Vitest + React Testing Library (RTL) + TanStack Query + TanStack Router
套件 ：`npm i -D vitest-fetch-mock@0.3.0`

**核心測試邏輯**
整合測試的目標不是測實作細節（Implementation Details），而是測試 **「使用者行為流程」**：

1. **Arrange:** 準備假環境 (Mock API、Router Context)。
2. **Act:** 模擬使用者填寫表單、點擊按鈕。
3. **Assert:** * **對使用者：** 驗證畫面是否出現「成功訊息」。
    - **對開發者：** 驗證程式是否真的發出正確的 API 請求 (Spy)。

```jsx
// 檔案：src/__tests__/contact.lazy.test.jsx
import { render, screen } from "@testing-library/react";
import { expect, test, vi } from "vitest";
import createFetchMock from "vitest-fetch-mock";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
// 從路由定義檔匯入 Route 物件，目標是測試裡面的 Component
import { Route } from "../routes/contact.lazy";

// 1. 初始化 Mock 工具 (把 Vitest 的工具箱 vi 借給 fetch-mock 用)
const fetchMocker = createFetchMock(vi);
fetchMocker.enableMocks();

test("can submit contact form", async () => {
  // === Step 1: Arrange (佈置環境) ===
  
  // 設定 API 假回應 (Spy): 下一次 fetch 呼叫時，直接回傳這個 JSON，不要真的連網
  fetchMocker.mockResponseOnce(JSON.stringify({ status: "ok" }));

  // 建立全新的 QueryClient (因為實際 code 有用到 useContext，在測試時也要寫)
  const queryClient = new QueryClient();

  // Render 元件
  // 必須包裹 QueryClientProvider (因為實際 code 有用到 useContext，在測試時也要寫)
  // Route.options.component 就是把真正的 Component 從路由設定檔挖出來
  render(
    <QueryClientProvider client={queryClient}>
      <Route.options.component />
    </QueryClientProvider>
  );

  // 準備測試資料
  const testData = {
    name: "Brian",
    email: "test@example.com",
    message: "Hello World",
  };

  // === Step 2: Act (模擬使用者行為) ===

  // 模擬填寫表單 (RTL 哲學：用 Placeholder 抓元素，模擬使用者視覺)
  const nameInput = screen.getByPlaceholderText("Name");
  const emailInput = screen.getByPlaceholderText("Email");
  const msgTextArea = screen.getByPlaceholderText("Message");

  nameInput.value = testData.name;
  emailInput.value = testData.email;
  msgTextArea.value = testData.message;

  // 模擬點擊按鈕
  const btn = screen.getByRole("button");
  btn.click(); // 這會觸發 onSubmit -> 執行 fetch

  // === Step 3: Assert (驗證結果) ===

  // A. 驗證 UI (Async): 等待 "Submitted" 標題出現
  // findByRole 會輪詢 (Polling) DOM，直到元素出現或超時
  const h3 = await screen.findByRole("heading", { level: 3 });
  expect(h3.innerText).toContain("Submitted");

  // B. 驗證 API 行為 (Spy): 檢查間諜的小本本
  const requests = fetchMocker.requests();
  
  // 確保按鈕按一次，API 只打一次 (防手抖、防邏輯錯誤)
  expect(requests.length).toBe(1);
  
  // 確保打對 URL
  expect(requests[0].url).toBe("/api/contact");

  // 確保送出的資料格式正確 (Header, Body)
  expect(fetchMocker).toHaveBeenCalledWith("/api/contact", {
    body: JSON.stringify(testData),
    headers: { "Content-Type": "application/json" },
    method: "POST",
  });
});```

### 解析 & QA
#### Q1: 為什麼需要 `createFetchMock(vi)`？為什麼不直接寫假資料？

- **原因：** 因為實際 code 有 call API （程式碼內部寫的是 `fetch()`），在測試環境 (Node.js) 執行會報錯，所以要用 `createFetchMock(vi)`。

- **Spy 的作用：** 我們需要一個「間諜」來攔截 `fetch`。
    1. **阻斷連線：** 防止真的打出去。
    2. **假裝回應：** 回傳 `{ status: "ok" }` 讓前端跑完流程。
    3. **紀錄行為：** 紀錄「你有沒有打電話？」、「參數對不對？」。如果不 Mock，我們就無法驗證 `POST` 的資料是否正確。
  
- **註：** 如果用 Axios，則需用 `vi.mock('axios')`。

#### Q2: 為什麼檔名要有 `Lazy`？測試也要 Lazy？

- **解答：** 完全不用。
- **原因：** 純粹是 **TanStack Router** 的檔案命名慣例 (`contact.lazy.jsx`)。測試檔取名 `contact.lazy.test.jsx` 只是為了在檔案總管排在一起好找而已。

#### Q3: `screen.findByRole` 是什麼？為什麼不抓 `<h3>`？

- **`findBy...` vs `getBy...`：**
    - `getBy`：找不到馬上報錯（適合測一開始就在的東西）。
    - `findBy`：**回傳 Promise**，會等待元素出現（適合測 API 回來後的 Async UI 更新）。
- **`Role`：** 強制你寫語意化 HTML。`{ level: 3 }` 對應 `<h3>`。這能確保你的網頁對螢幕閱讀器是友善的。
#### Q4: `Route.options.component` 是什麼黑魔法？
- **原理：** `import { Route } from ...` 匯入的是一個由 TanStack Router 產生的**設定物件**。
- **結構：** 真正的 React Component 被包在該物件的 `options.component` 屬性裡。
- **識別：** 程式是靠 `import` 的**檔案路徑**來區分是哪一頁的元件，而不是靠變數名稱。
    
#### Q5: `vi` 是什麼？
- **解答：** Vitest 的工具箱 (Utility Object)。
- **用途：** 用來製造間諜 (Spy)、控制時間 (Fake Timers)、模擬模組 (Mocks)。`fetch-mock` 需要借用 `vi` 的能力來實作攔截功能。
#### Q6: 假如測試有 Call 不同的 API，它怎麼知道是哪一次？

問到痛點了！這就是 `toHaveBeenCalledWith` 的特性與限制。
**情境：** 假設你的元件依序呼叫了兩支 API：
1. POST `/api/login`
2. GET `/api/user`

##### **寫法 1：使用 `toHaveBeenCalledWith` (寬鬆檢查)**
```jsx
// 檢查「有沒有」呼叫過 login？ (有就好，不管它是第幾次)
expect(fetchMocker).toHaveBeenCalledWith("/api/login", ...); // PASS

// 檢查「有沒有」呼叫過 user？ (有就好)
expect(fetchMocker).toHaveBeenCalledWith("/api/user", ...);  // PASS
```
##### **寫法 2：使用 `toHaveBeenNthCalledWith` (嚴格順序檢查)** 如果你在乎順序，或者 URL 很像，怕混淆，要用 `Nth` (第 N 次)：
```jsx
// 斷言：第 1 次呼叫必須是 Login
expect(fetchMocker).toHaveBeenNthCalledWith(1, "/api/login", {
  method: "POST",
  // ...
});

// 斷言：第 2 次呼叫必須是 User
expect(fetchMocker).toHaveBeenNthCalledWith(2, "/api/user", {
  method: "GET",
  // ...
});
```

##### **寫法 3：手動檢查 `requests` 陣列 (最直觀，推薦給資深者)** 這就是你在上一段 code 看到的寫法，這其實最靈活：
```jsx
const requests = fetchMocker.requests();

// 檢查第一次
expect(requests[0].url).toBe("/api/login");
expect(requests[0].method).toBe("POST");

// 檢查第二次
expect(requests[1].url).toBe("/api/user");
expect(requests[1].method).toBe("GET");
```

## Testing Custom Hooks 

+ hook
	```jsx
	import { useState, useEffect, useDebugValue } from "react";

	export const usePizzaOfTheDay = () => {
  const [pizzaOfTheDay, setPizzaOfTheDay] = useState(null);

  // Debug 用：這行只會在 React DevTools 顯示，不影響邏輯
  useDebugValue(pizzaOfTheDay ? `${pizzaOfTheDay.id}` : "loading ...");

  useEffect(() => {
    async function fetchPizzaOfTheDay() {
      const response = await fetch("/api/pizza-of-the-day");
      const data = await response.json();
      
      // 更新 State：這會觸發 Component 重新渲染 (Re-render)
      setPizzaOfTheDay(data);
    }

    fetchPizzaOfTheDay();
  }, []); 

  // 4. 回傳當下的 State (第一次是 null，第二次 Re-render 後才有資料)
  return pizzaOfTheDay;
};

	```
+ Testing
	```jsx
	import { expect, test, vi } from "vitest";
	import { renderHook, waitFor } from "@testing-library/react"; 
	import createFetchMock from "vitest-fetch-mock";
	import { usePizzaOfTheDay } from "../usePizzaOfTheDay";
	
	// --- 設定 Fetch Mock 環境 (攔截網路請求) ---
	const fetchMocker = createFetchMock(vi);
	fetchMocker.enableMocks();
	
	// --- 準備假資料 (Mock Data) ---
	const testPizza = {
	  id: "calabrese",
	  name: "The Calabrese Pizza",
	  category: "Supreme",
	  description: "lol pizza from Calabria",
	  image: "/public/pizzas/calabrese.webp",
	  size: { S: 12.25, M: 16.25, L: 20.25 },
	};
	
	// --- 測試案例 1: 驗證初始狀態（同步 case） ---
	test("gives null when first called", async () => {
	  // 1. 告訴 Fetch 這次要回傳什麼假資料
	  fetch.mockResponseOnce(JSON.stringify(testPizza));
	
	  // 2. 執行 Hook (在測試環境模擬 React 運作)
	  const { result } = renderHook(() => usePizzaOfTheDay());
	
	  // 3. 驗證 First Call：此時 API 還在跑，所以應該是 null
	  expect(result.current).toBeNull();
	});
	
	// --- 測試案例 2: 驗證 API 回來後的狀態（非同步 case） ---
	test("to call the API and give back the pizza of the day", async () => {
	  fetch.mockResponseOnce(JSON.stringify(testPizza));
	  
	  const { result } = renderHook(() => usePizzaOfTheDay());
	
	  // 1. 等待更新 有 await 很重要 (Wait For)
	  await waitFor(() => {
	    // 2. 驗證 State 已經變成我們的假資料了
	    expect(result.current).toEqual(testPizza);
	  });
	
	  // 3. 驗證 Fetch 確實有被呼叫，且 URL 正確
	  expect(fetchMocker).toBeCalledWith("/api/pizza-of-the-day");
	});
	```
### 觀念補充

#### 為什麼 API 請求放在 `useEffect`？

- **Vue 直覺**：在 `setup()` 裡直接寫 `fetch()`，因為 `setup` 只會在元件初始化時執行**一次**。
- **React 真相**：Function Component 每次渲染（Render）都會**從頭到尾重新執行**整個函式。
    - 如果不包在 `useEffect`：`Render` -> `Fetch` -> `setState` -> `Re-render` -> `Fetch`... (**無限迴圈**)
    - 包在 `useEffect([], ...)`：告訴 React「這段程式碼只在**掛載後 (Mounted)** 執行一次」。
    - **useEffect 在 render 完之後才會執行**

#### Vue vs React：架構與測試思維差異

##### 為什麼 React 測試比較「麻煩」？

- **Vue (白箱測試)**：
    - Vue 的響應式系統 (Reactive System) 是基於物件代理 (Proxy)，脫離元件也能運作。
    - 測試可以直接呼叫 function 或修改 `ref.value`。
    
- **React (黑箱測試)**：
    - Hooks (`useState`, `useEffect`) 高度依賴 React Runtime
    - 必須用 `renderHook` 來模擬完整的瀏覽器渲染流程。
        
##### State 更新的禁忌

- **Vue**: 可以在 `setup` 根目錄直接修改變數 (e.g., `count.value++`)，沒問題。
- **React**: **絕對禁止**在 Component Function Body 直接呼叫 `setState`，這會導致無限迴圈。
    
    - ✅ 合法地點 1：**Event Handlers** (onClick, onChange...) - 最常用。
    - ✅ 合法地點 2：**useEffect** (API, 訂閱) 



npm i -D @vitest/coverage-v8@2.1.3
npm i -D @vitest/ui@2.1.3
npm i -D @vitest/browser@2.1.3 playwright@1.48.0 vitest-browser-react@0.0.1

## Snapshot Testing
簡單來說，這是在幫你的 Component **「拍一張照片」** 並存檔
```jsx
test("snapshot with nothing in cart", () => {
  // 1. 渲染元件
  const { asFragment } = render(<Cart cart={[]} />);
  
  // 2. 拍快照並比對
  expect(asFragment()).toMatchSnapshot();
});
```
- **`asFragment()`**：把目前渲染出來的 DOM 結構變成一段文字（類似 HTML 字串）。
- **`toMatchSnapshot()`**：
    - **第一次跑測試時**：Vitest 會自動在旁邊生出一個 `__snapshots__` 資料夾，裡面有一個檔案把這段 HTML 存起來（這就是「快照」）。
    
    - **第二次以後跑測試**：它會把「這次渲染的 HTML」跟「上次存的檔案」做比對。
        - **完全一樣** 🟢 通過。
        - **差一個字** 🔴 失敗 (報錯說 DOM 改變了)。

### Snapshot Testing 的意義是什麼？

它的核心價值是 **「防止意外的 UI 變更 (Regression Testing)」**。

**想像一個情境：** 你寫了一個很複雜的 `Cart` 元件，裡面有一堆 class 和結構。 某天你不小心改到了共用的 CSS 或是誤刪了一行 code，導致 `Cart` 的版跑掉了。

- **沒有快照**：你可能要跑起來看畫面才發現（或者根本沒發現）。
- **有快照**：測試會直接報錯：「嘿！原本這裡是 `<div>`，怎麼變成 `<span>` 了？」，強迫你去檢查這個改變是不是你預期的。

### 部分人不鼓勵 Snapshot Testing?

#### ❌ 不被鼓勵的原因（為什麼大家後來討厭它）：

1. **太敏感 (Brittle)**： 你只是改了一個無關緊要的 class (例如 `margin-top: 10px` 改成 `12px`)，或者改了一點點文案，測試就掛了。這會造成很多「假警報」。
    
2. **開發者的惰性 (The "Update" Trap)**： 這是最致命的。當測試失敗時，工具會問你：「要更新快照嗎？(Press 'u' to update)」。 因為變動太頻繁，很多工程師根本**不看**哪裡變了，直接無腦按 `u` 更新快照讓測試通過。
    
    > **結果：** 快照測試變成了「只要程式碼沒報錯就通過」，失去了「檢查 UI 正確性」的意義。
    
3. **沒有測到行為**： 它只測了「結構長怎樣」，沒測「按鈕能不能按」、「資料對不對」。
    

#### ✅ **特定場景還是好用**

 它適合拿來測靜態結構，對於那些 **「永遠不應該變動」** 的靜態元件（例如：法律條款頁面、設定檔 Config Object），Snapshot 還是最快能確保它沒被亂改的方法。

## Vitest Plugins
### A. Code Coverage (`@vitest/coverage-v8`)

- **它是什麼：** 這是用來檢查 **「你的測試到底覆蓋了多少程式碼」** 的工具。
- **功能：** 跑完測試後，它會告訴你：「嘿，你寫了 100 行程式碼，但測試只跑過了其中 60 行，剩下的 40 行 (40%) 從來沒被執行過，可能有 Bug 哦！」
- **原理：** 利用 V8 引擎（跟 Chrome 一樣的核心）來即時追蹤程式碼執行狀況。
### B. Vitest UI (`@vitest/ui`)

- **它是什麼：** 一個網頁版的圖形介面。
- **功能：** 你不用只看終端機 (Terminal) 黑底白字的 Log。打指令 `npx vitest --ui` 後，它會開一個網頁：
    
    - 可以看到所有測試檔案的清單。
    - 可以看到測試失敗的具體報錯。
    - **最酷的功能**：它有一個 **Module Graph**，能畫出你的 Component 依賴了誰，視覺化你的程式架構。
        

### C. Browser Mode (`@vitest/browser` + `playwright` + `vitest-browser-react`)

- **它是什麼：** 讓你的單元測試在 **「真的瀏覽器」** 裡跑，而不是在 Node.js 的模擬環境 (JSDOM) 裡跑。
    
- **為什麼需要：**
    - 一般 React 測試 (RTL) 是跑在 **JSDOM**（一個用 JS 寫的假瀏覽器環境）。它很快，但有些東西它不支援（例如：Canvas 繪圖、複雜的 CSS Layout 計算、某些新的 Web API）。
        
    - 裝了這個，Vitest 就會呼叫 **Playwright** 去開一個無頭瀏覽器 (Headless Chrome)，在裡面跑你的測試 code。
        
- **關係：** `vitest-browser-react` 是用來讓 Vitest 懂得怎麼在瀏覽器裡渲染 React 元件的橋樑。

## React 19
##  "use client" & "use server"
**React 19 才正式推出**，Next.js 之前就可以用是為了提早支援 Server Components，當時使用的是 React 的實驗性版本。
## ## Web Components 一級支援

## Form Actions
核心變革：從 `onSubmit` 到 `action`

React 19 引入了對 `<form>` 元素的原生支援，目的是**減少樣板程式碼 (Boilerplate)**，讓 React 自動處理表單的繁瑣操作。

### 語法對照表

| **特性**   | **舊版 React (React 18 以前)** | **React 19 Actions**     |
| -------- | -------------------------- | ------------------------ |
| **觸發屬性** | `<form onSubmit={save}>`   | `<form action={save}>`   |
| **阻止重整** | 必須手動寫 `e.preventDefault()` | **React 自動處理** (不用寫了)    |
| **獲取資料** | `new FormData(e.target)`   | **自動注入** `formData` 作為參數 |
| **參數類型** | Event Object (`e`)         | `FormData` Object        |
### 範例
```jsx
// ❌ 舊寫法 (Manual)
function addToCart(e) {
  e.preventDefault(); // 很煩，每次都要寫
  const formData = new FormData(e.target); // 很煩，還要自己挖
  // ... fetch api
}

// ✅ React 19 新寫法 (Automatic)
function addToCart(formData) {
  // 直接拿資料開始做事！
  const id = formData.get("productId");
  // ... fetch api
}
// 從 onSubmit 變 action
return <form action={addToCart}>...</form>;
```

### 補充 'user server'
`action={fn}`，它可以是前端的 API 請求，加上 `"use server"` 也可以是直接執行後端資料庫邏輯（Server Actions）寫 sql 語法，
### useFormStatus

#### 用途
這是一個專門用來解決「Loading 狀態傳遞地獄 (Prop Drilling)」**的新 Hook。
#### 解決了什麼問題？

- **痛點**：以前若表單正在提交 (Pending)，你想把深層的 `<Input>` 或 `<Button>` 設為 `disabled`，你必須在最外層 `<form>` 宣告一個 `isLoading` state，然後一層一層傳下去 (`Form -> div -> div -> Input`)。
    
- **解法**：`useFormStatus` 就像一個無線電接收器。只要在 `<form>` **內部**的任何子元件呼叫它，就能自動偵測父層表單的狀態。

```jsx
// 不需要從父層接收 props！
function SubmitButton() {

  // 自動往上找最近的 <form> 問狀態
  const { pending } = useFormStatus(); 
  return <button disabled={pending}>Add to Cart</button>;
}
```

## use & Suspense
### 父元件：架構與保護網

```jsx
function ErrorBoundaryWrappedPastOrderRoutes(props) {
  const [page, setPage] = useState(1);

  // 1. 【關鍵】：獲取 Promise
  // 這裡不解構 { data, isLoading }，而是直接拿 .promise
  // 前提：tanstack query 需設定 experimental_prefetchInRender: true，讓它在 Render 期間就產生 Promise
  const loadedPromise = useQuery({
    queryKey: ["past-orders", page],
    queryFn: () => getPastOrders(page),
    staleTime: 30000,
  }).promise; 

  return (
    // 2. 【第一層保護】：錯誤邊界
    // 當 loadedPromise 被 reject (API 失敗) 時，這裡會攔截錯誤
    <ErrorBoundary fallback={<div>Something went wrong!</div>}>
      {/* 3. 【第二層保護】：Loading 邊界 */}
      {/* 當 loadedPromise 還在 Pending 時，React 會顯示這個 fallback */}
      <Suspense
        fallback={
          <div className="past-orders">
            <h2>Loading Past Order ...</h2>
          </div>
        }
      >
        {/* 4. 【傳遞 Promise】：像傳遞接力棒一樣傳給子元件 */}
        <PastOrdersRoute
          loadedPromise={loadedPromise}
          page={page}
          setPage={setPage}
          {...props}
        />
      </Suspense>
    </ErrorBoundary>
  );
}
```

### 子元件

```jsx
// 接收父元件傳來的 loadedPromise
function PastOrdersRoute({ page, setPage, loadedPromise }) {
  // 1. 【關鍵】：使用 React 19 的 use()
  // 這行程式碼會「暫停」元件渲染，直到 Promise 解開
  // - Pending: 暫停 -> 顯示父層 Suspense fallback
  // - Resolved: 恢復 -> 把資料塞進 data
  // - Rejected: 崩潰 -> 觸發父層 ErrorBoundary
  const data = use(loadedPromise);

  // --- 程式碼執行到這裡，代表 data 一定有值 (Happy Path) ---

  const [focusedOrder, setFocusedOrder] = useState();
  
  // (下略：其他的 query 邏輯...)
  // ...
  
  return (
    // 這裡可以直接 render data，完全不用判斷 isLoading
    <div>
        {/* 使用 data 渲染畫面 */}
    </div>
  );
}
```

### 觀念補充
#### 1. `useQuery(...).promise` 是什麼？

這其實是 **TanStack Query (React Query)** 提供的一個特殊屬性（特別是為了配合 React Suspense 模式）。

- **一般模式**：`useQuery` 回傳 `data`, `isLoading`, `error`。你必須自己寫 `if (isLoading) return <Loading />`。
    
- **Promise 模式**：
    - 所以我們不看 `data` 或 `isLoading`。
    - 以 **Promise ** 本身判斷（也就是 `loadedPromise`）。        

==**`loadedPromise` 的作用**：我們把這個把它交給 React 的新功能 `use()` 去處理。==

#### 2. `use` 是幹嘛用的？(React 19 新功能)

你可以把 `use(promise)` 想像成一個 **「會自動暫停元件渲染的 `await`」**。

- **以前的寫法 (條件渲染)**：
    ```jsx
    const { data, isLoading } = useQuery(...);
    if (isLoading) return <Spinner />; // 手動切換 UI
    return <div>{data.title}</div>;
    ```
    
    缺點：每個元件都要重複寫這段判斷，而且資料載入邏輯跟 UI 綁死。
    
- React 19 寫法 (`use` + Suspense)**：
    ```jsx
    // 當 React 執行到這一行...
    const data = use(loadedPromise); 
    // 如果 Promise 還沒解開 (Pending)，React 會直接「暫停」渲染這個元件，
    // 並自動往上找最近的 <Suspense> 顯示 fallback。
    // 等到 Promise 解開 (Resolved)，React 會「恢復」渲染，並把資料塞進 data。
    
    return <div>{data.title}</div>; // 這裡永遠只會拿到「成功後的資料」
    ```
    
**優點**：你的 Component 變得非常乾淨！程式碼裡面完全沒有 `if (isLoading)`，因為只要能執行到 `return`，資料一定已經準備好了。

#### 3. `Suspense` 與 `ErrorBoundary` 的角色分工

- **`<Suspense>` (管 Loading)**：
    - 當 `use(promise)` 發現資料還在載入時，元件會「暫停」。
    - `Suspense` 負責接手，顯示 `fallback` (例如：Loading Past Order ...)。
    - **一句話：它處理「等待中」的狀態。**
        
- **`<ErrorBoundary>` (管 Error)**：
    - 如果 `use(promise)` 發現這個 Promise **失敗了 (Rejected)**，`ErrorBoundary` 負責接手顯示錯誤畫面 (例如：Oops, something went wrong)
    - **一句話：它處理「Render error」的狀態。**
        
這兩者通常會包在一起使用（Async Boundary），形成一個完整的非同步保護網。

#### 4. 為什麼要改用這種寫法？原本的 `isLoading` 不好嗎？

原本的 `isLoading ? <Loading /> : <Content />` 當然能用，但在大型應用中有兩個缺點：

1. **Waterflow (瀑布流) 問題**： 如果是傳統寫法，父元件 render -> 抓資料 -> isLoading -> 抓完 -> render 子元件 -> 子元件抓資料... 會變成一階一階的慢。 使用 Suspense 模式，可以更容易實現「盡早抓取 (Render-as-you-fetch)」，讓 React 更聰明地調度渲染時機。
    
2. **程式碼分離 (Separation of Concerns)**：
    - **UI 元件 (`PastOrdersRoute`)**：只專注於「有資料時要怎麼顯示」。
    - **父元件/架構層 (`Suspense`/`ErrorBoundary`)**：專注於「載入中或出錯時要怎麼辦」。
        
    這樣你的 UI 元件會變得非常「純粹」且好測試。

#### 5. fetch on mount VS fetch on render
##### Fetch-on-Mount (掛載後抓取)
- **機制**：Render  -> 執行 `useEffect` -> 發出請求。
- **關鍵字**：`useEffect`、TanStack `useQuery`。
- **Vue 對照**：`onMounted(() => fetch())`。
- **缺點**：會有瀑布流 (Waterfall) 問題。

##### Fetch-on-Render / Render-as-You-Fetch (渲染時抓取) — React 19 新潮

- **機制**：開始 Render  (**立刻發出請求 (丟出 Promise)** )-> 暫停 (Suspend) -> 顯示 Fallback。
- **關鍵字**：`Suspense`、`use()`、`experimental_prefetchInRender` for TanStack Query。
- **Vue 對照**：`script setup` 頂層 `await fetch()` (搭配 Vue Suspense)。
- **優點**：早點開始抓，體驗較好。

## React Compiler
開發者不再需要手動寫 `useMemo` 與 `useCallback`，效能優化變成編譯器的責任。

- **支援版本**：React 17, 18, 19 (官方強烈建議 18+)。
- **穩定性**：已在 Instagram、Facebook 的 Production 環境運行。

- **React 19**：原生支援，**不需要**額外安裝 runtime 套件。
- **React 17 / 18**：核心不認識 Compiler 產出的快取語法，**必須**額外安裝 `react-compiler-runtime` 這個 Polyfill。

### 1. 環境檢查 (Health Check)

在安裝前，先檢查專案代碼庫是否相容

```
npx react-compiler-healthcheck@beta
```

- **成功指標**：顯示大部分組件可被優化 (Successfully compiled X of Y components)，且無不相容的 Libraries。
    
### 2. 安裝 Compiler

目前仍為 Beta 版，可能會有版本依賴警告，建議加上 `--force`。

```
npm install -D babel-plugin-react-compiler@beta --force
```

### 3. 配置 (以 Vite 為例)

修改 `vite.config.js`，在 React plugin 的 Babel 設定中加入 Compiler。

JavaScript

```jsx
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [
          ["babel-plugin-react-compiler", { target: "19" }] // 若是 React 18 可改為 "18"
        ],
      },
    }),
  ],
})
```

### 4. 驗證是否生效 (React DevTools)

這是最重要的一步，確認 Compiler 是否真的在工作。

1. 啟動專案 (`npm run dev`)。
    
2. 開啟瀏覽器開發者工具 (F12)。
    
3. 切換到 **Components** 分頁 (React DevTools)。
    
4. 觀察組件樹狀圖。
    

**成功訊號**： 你會在 Component 名稱旁邊看到一個 **「Memo (✨ 魔法棒/星星)」** 的標記。

這代表 React Compiler 已經自動對該組件進行了 Memoization 優化。
### 5. 其他指令

- **手動排除 (Escape Hatch)**：如果不希望某個組件被優化，可在檔案頂部加上指令：
    
    ```
    "use no memo";
    ```
    