| JS 觀念(來自前三篇) | 在 React 裡長成什麼 | 已經寫在哪(完整路徑) | 狀態 |
|---|---|---|---|
| 物件可變、比較的是參考 | setState(user) 直接 mutate 不重繪 |
frontend-docs/javascript/JS_Core_and_Runtime/比較三兄弟-鬆散等於-嚴格等於-Object-is-與React-Vue的狀態比對.md |
✅ 已寫透,206 行,含 React 兩條路徑與 Vue 的 hasChanged 對照 |
== / === / Object.is 的差異 |
React 用 Object.is 做 bailout 判斷 |
同上(第三節「React 的兩條路徑」h–l) | ✅ 已寫透,還有配圖 frontend-docs/javascript/JS_Core_and_Runtime/images/學習JS_圖解_比較三兄弟與React重繪判斷路徑_2026-09-05.svg |
| 閉包、Environment Record、變數活多久 | Hook 的 stale closure(過期閉包) | frontend-docs/react/useState底層-Fiber-Tree-memoizedState與過期閉包.md |
✅ 已寫透,323 行,含 memoizedState 單向鏈表與 key 的真正作用 |
useRef 取代閉包私有變數 |
timerRef.current 跨 render 存活 |
debounce-與-React計時器/debounce為什麼一定要setTimeout-以及React的useRef.md 第 2 節 |
✅ 本系列第 2 篇 |
| getter / setter 與 Proxy | 為什麼 React 不像 Vue 自動偵測 | iThome鐵人賽-2026/草稿-useState為什麼沒更新-從getter與Proxy看React的設計選擇.md |
✅ 已寫透,464 行,五星,含 useRef.current vs Vue ref.value 的對照 |
| 函式的純粹性 | render 必須是純函數、StrictMode 跑兩次 | frontend-docs/react/01-React-純函數與嚴格模式-StrictMode.md |
✅ 442 行 |
| JSX 其實是函式呼叫 | createElement / jsx-runtime |
編譯與打包-學習路徑/05-JSX轉譯機制-createElement與jsx-runtime-Babel與SWC三步驟.md |
✅ 211 行 |
| effect 的 cleanup 就是 clearTimeout | Render 與 Commit 兩階段、Unmount 時機 | frontend-docs/react/React兩階段渲染-Render與Commit-Mount-Update-Unmount生命週期.md |
✅ 187 行 |
稀疏陣列與 Array(5) |
.map() 產生元件清單、key 規則 |
JS-資料型別/JavaScript資料型別總覽-原始型別與物件.md 第 8-b 節 |
✅ 本系列第 1 篇 |
| truthy / falsy | ⚠️ JSX 條件渲染的 0 陷阱 |
❌ 沒有任何一篇寫過 | 👇 補在第 2 節 |
| 哪些值 React 會「渲染出來」 | ⚠️ {null} 不渲染但 {0} 會 |
❌ 沒有任何一篇寫過 | 👇 補在第 3 節 |
| 物件不能當 React child | ⚠️ Objects are not valid as a React child |
❌ 沒有任何一篇寫過 | 👇 補在第 4 節 |
0 陷阱(最常見的 falsy 事故)這是把「truthy / falsy」直接踩爆的地方,而且畫面上會冒出一個孤零零的 0,看起來像 UI 壞掉。
// ❌ 購物車是空的時候,畫面上會出現一個「0」
function Cart({ items }) {
return (
<div>
{items.length && <ItemList items={items} />}
</div>
);
}
為什麼:&& 運算子不回傳布林值,它回傳的是「決定結果的那個運算元」。
items.length 是 0(falsy)→ && 短路,回傳 0 本身
0,而 React 會把數字渲染出來
0 && <ItemList /> // 回傳 0 → 畫面出現 0
"" && <ItemList /> // 回傳 "" → 空字串,看不見(但仍是被渲染的)
null && <ItemList /> // 回傳 null → 不渲染,安全
undefined && <ItemList />// 回傳 undefined→ 不渲染,安全
NaN && <ItemList /> // 回傳 NaN → 畫面出現「NaN」
⚠️ 注意 8 個 falsy 值裡有 3 個會被渲染出來(0、NaN、""),另外 5 個安全(false、null、undefined、-0 會顯示 0、0n 會顯示 0)—— 實際上只有 false、null、undefined 是真正安全的。
// ✅ 方法 1:把條件變成真正的布林(最推薦,意圖最明確)
{items.length > 0 && <ItemList items={items} />}
// ✅ 方法 2:用雙重否定強制轉成布林
{!!items.length && <ItemList items={items} />}
// ✅ 方法 3:用三元運算子(要處理「否則顯示什麼」的時候用這個)
{items.length ? <ItemList items={items} /> : <EmptyState />}
⚠️ 不要用 Boolean(items.length) && ... —— 能動,但比 > 0 多打好幾個字又沒有比較清楚。
JS-資料型別/JavaScript資料型別總覽-原始型別與物件.md 第 4 節列了完整的 8 個 falsy 值,第 4-b 節解釋了「== 不是用 truthy / falsy 判斷的」。這裡是同一組知識在 JSX 裡的實際事故現場。
這張表沒有任何一篇筆記整理過,但它是理解上面那個陷阱的根本:
| 值 | React 的行為 | 畫面上看到什麼 |
|---|---|---|
"abc" |
渲染 | abc |
123 |
渲染 | 123 |
0 |
渲染 | 0 ⚠️ |
NaN |
渲染 | NaN ⚠️ |
"" |
渲染(但是空的) | 什麼都沒有 |
null |
不渲染 | 什麼都沒有 ✅ |
undefined |
不渲染 | 什麼都沒有 ✅ |
true / false |
不渲染 | 什麼都沒有 ✅ |
[1, 2, 3] |
逐項渲染 | 123(⚠️ 陣列項目要 key) |
{ a: 1 } |
丟錯 | Error: Objects are not valid as a React child |
Symbol("s") |
丟錯 | 同上 |
10n(BigInt) |
依版本而異,通常丟錯 | 建議先自己 String(bigintValue) |
記憶口訣:只有 null、undefined、boolean 是「安靜的」,其他原始型別都會現形,物件則直接爆炸。
false 不渲染但 0 會這是 React 刻意的設計取捨:&& 短路在大多數情況下回傳的是 false,如果 false 也被渲染成「false」三個字,那全世界的條件渲染都會壞掉。但 0 是個合法的、使用者可能真的想顯示的數字(例如「庫存:0」),React 沒有立場替你決定要不要顯示它。
所以 0 的陷阱不是 React 的 bug,是 && 運算子的語意 —— 責任在 JS 那一邊。
Objects are not valid as a React childconst user = { name: "Abby", age: 20 };
return <div>{user}</div>;
// ❌ Error: Objects are not valid as a React child (found: object with keys {name, age})
為什麼會擋:React 需要把值變成文字節點,但物件的預設 toString() 只會給你 "[object Object]" —— 那對使用者完全沒有意義。與其默默印出一坨沒用的字,React 選擇直接丟錯讓你在開發階段就發現。
最常見的三個觸發情境:
{user} 應該是 {user.name}
{error} 應該是 {error.message}
{new Date()} 會丟錯,要 {date.toLocaleDateString()}
⚠️ Date 是最容易漏的那個,因為它「看起來很像可以直接顯示的東西」。
JS-資料型別/追問-BigInt取捨-不可變性-與包裝物件.md 第 5 節講的是「原始型別靠臨時包裝物件才有方法」,而這裡是反過來 —— 物件在需要變成原始值的場合(渲染成文字)反而過不去。兩邊都是 ToPrimitive 這條線上的事。
算是它的零件,但不等於它。 你自己在 iThome鐵人賽-2026/草稿-useState為什麼沒更新-從getter與Proxy看React的設計選擇.md 的最後(「c. 響應式 = Proxy 嗎?」那段)已經下過結論:
不是。響應式是「效果」,Proxy 是「手段」。
這裡把那句話展開成可檢查的定義。
track(),它把「目前正在執行的 effect」記進這個屬性的訂閱清單。trigger(),然後丟進 queueJob 排程。只有 a 不算 reactive system,那只是「可以攔截」。三個湊齊才是。
| 框架 / API | 攔截用什麼 | 有依賴收集嗎 | 有觸發更新嗎 | 算 reactive system 嗎 |
|---|---|---|---|---|
| Vue 2 | Object.defineProperty(getter/setter) |
✅ | ✅ | ✅ 是 |
Vue 3 reactive() |
Proxy | ✅ | ✅ | ✅ 是 |
Vue 3 ref() |
getter / setter(RefImpl 的 get value) |
✅ | ✅ | ✅ 是 |
| Solid / Preact Signals / Angular Signals | 函式呼叫(count()) |
✅ | ✅ | ✅ 是 |
| Svelte 5 runes | 編譯期改寫 | ✅ | ✅ | ✅ 是 |
React useState |
完全沒有攔截 | ❌ | ❌ | ❌ 不是 |
⚠️ 注意第 1 與第 3 列:同樣是 getter / setter,Vue 2 和 Vue 3 的 ref() 都是 reactive system。所以「用什麼手段攔截」跟「算不算 reactive」是兩回事 —— 差別在有沒有 b 和 c。
因為它走的是完全不同的路線:pull-based(拉)而不是 push-based(推)。
【push-based:Vue / Solid / Signals】
你改了 state.count = 1
│
▼
set trap 被攔截
│
▼
查訂閱清單:「誰讀過 count ?」→ 找到 [畫面 A, computed B]
│
▼
只通知這兩個 ── 精準到單一節點,不需要 diff
│
▼
更新畫面 A 與 computed B
【pull-based:React】
你呼叫 setCount(1)
│
▼
React 只知道「有人說變了」,不知道是誰用到它
│ ← 因為它從來沒攔截過讀取,沒有訂閱清單
▼
把整個元件函式「從頭再跑一次」,產生新的 element 樹
│
▼
跟上一棵樹做 diff / reconciliation
│
▼
找出真的不一樣的地方,commit 到 DOM
Object.is 比新舊參考。這就是 frontend-docs/javascript/JS_Core_and_Runtime/比較三兄弟-鬆散等於-嚴格等於-Object-is-與React-Vue的狀態比對.md 講的事。push —— 同上,push 是原地改,參考沒變,Object.is 判定沒變。useRef 改 .current 不會重繪 —— 因為 ref 連「有人說變了」這個訊號都不發。React Compiler v1.0 已於 2025-10-07 正式發布(React Conf 2025)。它會自動幫你插入 memoization,效果上很像「React 變聰明了」,但它不是 reactive system:
useMemo / useCallback / React.memo 的邏輯所以到 2026 年為止,React 官方仍然沒有引入 signals,也沒有 runtime 的 reactivity。這是路線選擇,不是還沒做到。
關聯:
frontend-docs/vue/00-ref與reactive-響應式的兩種實作.md第三節與第四節把ref()(getter/setter)與reactive()(Proxy)拆得很清楚,配合這一節的三零件定義讀,兩邊會扣起來。
一句話:d 回答「為什麼」,f 回答「怎麼做」,先有動機再看實作才不會卡住。
setState,不能像 Vue 一樣直接改就好」。答案是 React 刻意不做攔截(見下一節)。setState 之後,Fiber 上的 memoizedState 鏈表發生了什麼」。iThome鐵人賽-2026/Day03-靜態方法-實例方法-存取器屬性-讀懂React原始碼的三行寫法.md 已經寫過);f 需要 Fiber、單向鏈表、閉包三個前置。⚠️ 修正:這份順序我第一版把 e、f 寫反了。useState底層 那篇的第四節是「過期閉包(Stale Closure)」,它需要閉包的心智模型當前置,所以應該排在 debounce與useRef 之後。已改正。
所有 URL 於 2026-09-05 查閱。
&& 的 0 陷阱官方說明)
.current 不觸發 re-render)