iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
Modern Web

Angular 工程師的 React 陣痛期:30 天心智模型重建系列 第 3

Day 3|從 @if/@for 到 JSX:畫面交還給 JavaScript

  • 分享至 

  • xImage
  •  

畫面多了一個 0

那天在做一個檔案上傳列表頁。空清單的時候不該顯示表格,很簡單的需求:

{files.length && <FileTable files={files} />}

但跑起來,畫面正中央出現一個孤零零的 0

沒有報錯、沒有警告,就是一個 0 安靜地待在那裡。我盯著愣了一下,第一個念頭是:「這個數字是誰印的?」

答案是我自己。而且這行程式碼在 Angular 裡根本不可能寫得出來。


一、Angular 的模板:一個故意被綁手綁腳的語言

先講 Angular。同樣的需求:

@if (files().length) {
  <app-file-table [files]="files()" />
}

這裡的 @if 看起來像 JavaScript,但它不是。Angular 的模板是一套獨立的語言,是語法糖,寫完之後由編譯器轉成 render 指令,跟你的 TypeScript 是兩個世界。

也因為是獨立語言,它可以對你設限。模板裡不能宣告變數、不能寫 if/else 陳述式、不能呼叫 window、不能塞一整段運算邏輯進去——你只能寫「表達式」,而且是被限制過的表達式。

這些限制不是偷懶,是刻意的。理由有三個:

編譯器能做最佳化。 因為語法有限,Angular 知道每個綁定的依賴是什麼,才有辦法做精準的變更偵測。

模板不會變成垃圾場。 你想在模板裡寫複雜商業邏輯?寫不了,只能搬回 .ts檔。這個限制救過很多專案。

AOT 能在編譯期抓錯。 型別對不上、property 拼錯,ng build 就會失敗,不用等到執行時。

最重要的是:@if 是控制流程,不是運算。 它不會「回傳一個值」,它只是決定那段 DOM 要不要存在。條件是 0 也好、'' 也好、null 也好,結果就是不渲染。不會有髒東西跑出來,就像不會有不負責任的PM亂搞事。


二、JSX 不是模板,是函式呼叫

React 這邊,第一件要重新理解的事情是:

JSX 不是模板語言,不是你認知的那種HMTL。它是 JavaScript 的語法糖。

這行:

<div className="box">Hello</div>

編譯之後長這樣(現代的 automatic runtime):

jsx('div', { className: 'box', children: 'Hello' })

就只是一個函式呼叫,回傳一個描述畫面的物件。沒有編譯期的模板語言,沒有專屬語法——它就是 JS。

所以沒有 @if,也不需要 @if。因為 JS 本來就有條件判斷,你直接用就好。

但代價來了:JSX 的大括號裡只能放「表達式」,不能放「陳述式」。

// ❌ 不行,if 是陳述式,沒有回傳值
<div>{ if (show) { <Panel /> } }</div>

// ✅ 三元運算子是表達式
<div>{ show ? <Panel /> : null }</div>

// ✅ && 也是表達式
<div>{ show && <Panel /> }</div>

這就是為什麼 React 的條件渲染滿滿都是 &&? :——不是大家喜歡這樣寫,是大括號裡只放得下表達式

Angular 的 @if 是控制流程;React 的 && 是一個會產生值的運算。這個差別,就是那個 0 的來源。


三、那個 0 是怎麼跑出來的

回到我的 bug。

JavaScript 的 && 從來就不是「布林運算」,它是短路取值:左邊為 falsy 就回傳左邊,否則回傳右邊。

0 && <FileTable />      // → 0      (不是 false!)
3 && <FileTable />      // → <FileTable />
'' && <FileTable />     // → ''
null && <FileTable />   // → null

所以當 files.length0,整個表達式的值就是數字 0。它被交給 React 去渲染。

而 React 的渲染規則是這樣的:

畫面上
false 不顯示
null 不顯示
undefined 不顯示
true 不顯示
0 顯示 0
NaN 顯示 NaN

看到問題了。0 是一個合法的、有意義的內容——React 沒有理由幫你吞掉它。你寫 {count} 想顯示數量是 0 的時候,它當然得印出來。

React 沒有做錯任何事。是我把一個數字丟給它,然後期待它猜到我要的是布林。更精確的講法是:不是 React 猜錯,是你(開發者)在心裡執行了一次型別轉換(number → boolean),但程式碼裡沒有真的寫出那個轉換。

三種修法

// 1. 明確比較(我現在都用這個)
{files.length > 0 && <FileTable files={files} />}

// 2. 強制轉布林
{!!files.length && <FileTable files={files} />}

// 3. 三元運算子,意圖最清楚
{files.length ? <FileTable files={files} /> : null}

第一種最好,因為它把「我要判斷的是數量大於零」這件事寫出來了。&& 前面放非布林值,在 React 專案裡幾乎都是 bug 的前兆。

順帶一提,else 也不見了

Angular 有 @else

@if (files().length) {
  <app-file-table />
} @else {
  <app-empty-state />
}

React 只能用三元:

{files.length > 0 ? <FileTable /> : <EmptyState />}

巢狀兩層以上會變得很難讀。多數人的做法是提早 return,或乾脆拆成兩個元件——這也是 React 元件通常比 Angular 元件細碎的原因之一。


四、@for 變成 .map()track 變成 key

清單渲染是同樣的故事:Angular 給你語法,React 要你自己用 JS。

@for (file of files(); track file.id) {
  <app-file-row [file]="file" />
} @empty {
  <app-empty-state />
}
{files.map(file => (
  <FileRow key={file.id} file={file} />
))}

trackkey 做的是同一件事:告訴框架「這一項是誰」,這樣資料變動時才知道哪些 DOM 可以重用、哪些要重建。沒有它,框架只能靠位置猜,猜錯的結果就是輸入框的內容跑到別人身上、動畫亂跳、狀態錯位。

兩個差異值得記:

Angular v17 之後 track 是強制的,不寫編譯不過。React 的 key 只會在 console 給你一個警告,跑起來照樣能動——然後在某個改動順序的場景突然壞掉。

@empty 沒有對應物。 React 要自己寫:

{files.length > 0
  ? files.map(f => <FileRow key={f.id} file={f} />)
  : <EmptyState />}

至於「用 index 當 key」這個經典陷阱,兩邊的後果是一樣的,就不多說了——只有清單順序永遠不變時才能用。


五、Pipe 也沒了

Angular 的模板不能寫複雜運算,所以給了你 pipe:

{{ file.createdAt | date:'yyyy/MM/dd' }}
{{ user$ | async }}

React 沒有 pipe,因為不需要——你直接呼叫函式:

{formatDate(file.createdAt, 'yyyy/MM/dd')}

更簡單、更直覺、不用註冊、不用 import 到模組裡。但少了兩件事。

一是 pure pipe 的快取。 Angular 的純 pipe 在輸入沒變時不會重算,React 這邊每次 render 都會真的執行一次。大部分情況無所謂,遇到重運算就得自己 useMemo

二是 | async 這個真的沒有等價物,而且是我目前最想念的東西。訂閱、解訂閱、變更偵測,一根管線全包。React 這邊要自己處理生命週期——這件事會在下週 RxJS 那幾天變成主戲。


今天的結論

Angular 的模板是一個有保護機制的 DSL。自己發明一套專門描述畫面的語法,它限制你能寫什麼,換來編譯期檢查、最佳化空間,以及「模板不會變成大型垃圾場」的保證。@if 是控制流程,不會有值漏到畫面上。

JSX 是沒有保護機制的 JavaScript。你可以在裡面做任何 JS 做得到的事,代價是 JS 的所有脾氣你也一併繼承了——&& 的短路取值、0 是 falsy 但也是合法內容、表達式與陳述式的分別。這些都不是 React 的規則,是 JavaScript 的規則。

昨天說「寫 React 需要更懂 JS 底層」,那個 0 就是最好的例子。它不是 React 的坑,是認知在常常使用某種方式下,對 && 的理解一直停留在「布林運算」——之前寫 Angular,較少有機會踩坑到浮出水面,因為模板不會讓我這樣寫。

框架的保護機制,有時候擋掉的不一定是錯誤,還有你發現自己不懂的機會。


上一篇
Day 2|我的 Service 呢?沒有 DI 的第一週
下一篇
Day 4 | 口袋少了生命週期鉤子,換成 useEffect 的陷阱
系列文
Angular 工程師的 React 陣痛期:30 天心智模型重建9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言