JSX 是什麼? JSX 是 JavaScript 的語法擴充,讓你在 JavaScript 檔案裡直接寫類似 HTML 的標記,例如 <h1 className="greeting">Hello</h1>。瀏覽器看不懂 JSX,所以要先經過 Babel、TypeScript 之類的編譯器轉成一般 JavaScript 才能執行。本文說明 JSX 的定位、轉譯後長什麼樣子、跟 HTML 的差異、大括號 {} 的用法,最後整理常見錯誤。
JSX 是什麼:JavaScript 裡的類 HTML 標記
React 官方文件提醒,JSX 與 React 是兩個不同的東西,常常搭配使用,但可以各自獨立:JSX 是語法擴充,React 是 JavaScript 函式庫。
JSX 的規格由 facebook/jsx 專案維護。規格寫明 JSX 是 ECMAScript 的類 XML 語法擴充,沒有定義任何語意,也不打算由瀏覽器或 JavaScript 引擎直接實作,而是由各種預處理器(transpiler)轉成標準 ECMAScript。換句話說,<h1> 在 JSX 裡變成什麼,是由使用它的工具決定;在 React 裡,它會變成建立 React 元素的函式呼叫。
React 讓標記與邏輯放在同一個檔案,官方的說法是:渲染邏輯與標記放在一起,每次修改都能保持同步。React 元件是一個 JavaScript 函式,回傳的 JSX 就是畫面上要顯示的內容。
export default function TodoList() {
return (
<>
<h1>Hedy Lamarr's Todos</h1>
<ul>
<li>Invent new traffic lights</li>
<li>Rehearse a movie scene</li>
</ul>
</>
);
}
專案建立與目錄結構見〈React 工具鏈規格解析:基於 Vite 的初始化流程、目錄架構與生產環境建構〉。
JSX 轉譯後長什麼樣子:瀏覽器看不懂 JSX
React 官方部落格說明:瀏覽器本身不認得 JSX,大多數 React 使用者會靠 Babel 或 TypeScript 這類編譯器,把 JSX 轉成一般 JavaScript;Create React App、Next.js 等預先設定好的工具鏈也在內部包含了 JSX 轉換。
轉換方式有兩種,官方稱為舊版與新版 JSX transform:
- 舊版(classic):把 JSX 轉成
React.createElement(...)呼叫,所以只要用到 JSX,檔案裡就必須有import React from 'react'。 - 新版(automatic):React 17 起提供,編譯器自動從
react/jsx-runtime匯入jsx之類的函式再呼叫,不必手動 import React。官方註明這些函式只給編譯器使用,自己手動建立元素時仍要用React.createElement;舊版 transform 不會被移除。
用 esbuild 轉換同一段 JSX,兩種輸出如下。這段腳本在 Node.js 執行,jsx: 'transform' 對應 classic,jsx: 'automatic' 對應 automatic:
const esbuild = require('esbuild');
const src = `function App() {
const name = 'Sean';
return <h1 className="greeting">Hello, {name}</h1>;
}`;
console.log(esbuild.transformSync(src, { loader: 'jsx', jsx: 'transform' }).code);
console.log(esbuild.transformSync(src, { loader: 'jsx', jsx: 'automatic' }).code);
console.log(esbuild.transformSync('const a = <><p>A</p><p>B</p></>;', { loader: 'jsx', jsx: 'automatic' }).code);

從輸出可以看到:
- classic 的結果是
React.createElement("h1", { className: "greeting" }, "Hello, ", name),標籤名稱成為第一個參數,屬性成為第二個參數,其餘是子節點。 - automatic 的結果是從
react/jsx-runtime匯入函式,子節點放進children屬性;這個例子的h1有兩個子節點(文字與變數),esbuild 產出的是jsxs。 <>...</>(Fragment 短語法)被轉成jsxs(Fragment, { children: [...] })。Fragment 在 DOM 裡不會留下節點,用它拆元件時 DOM 結構會怎麼變化,可前往〈React JSX 轉譯與 DOM 結構:Fragment 應用、多態元件 as 模式與層級錯位排查〉。
工具怎麼選 classic 或 automatic
各工具的設定名稱不同,但都有這兩種模式:
| 工具 | 設定 | 說明 |
|---|---|---|
Babel(@babel/preset-react) | runtime: 'classic' 或 'automatic' | 官方文件寫明預設為 automatic;automatic 自動匯入 JSX 轉出的函式,classic 不會自動匯入 |
| TypeScript | tsconfig 的 jsx 選項 | react 是 classic runtime,react-jsx 是 automatic runtime,另有 preserve、react-jsxdev、react-native;檔案要用 .tsx 副檔名 |
| esbuild | jsx: 'transform'/'automatic' | 上面範例使用的選項 |
React 官方部落格另外提到,新版 transform 由 React 17 引入,Babel 7.9.0 以上、TypeScript 4.1 以上支援。
JSX 的結果是一個 JavaScript 物件
JSX 轉譯後呼叫的函式會回傳 React 元素,本質上是描述畫面的物件。用 React.createElement 建立同一個元素,再把 type 與 props 印出來:
const React = require('react');
const el = React.createElement('h1', { className: 'greeting' }, 'Hello, Sean');
console.log(JSON.stringify({ type: el.type, props: el.props }));
{"type":"h1","props":{"className":"greeting","children":"Hello, Sean"}}
這也是官方解釋「為什麼多個 JSX 標籤要包起來」的理由:JSX 底層是純 JavaScript 物件,函式不能同時回傳兩個物件而不包進陣列,所以也不能同時回傳兩個 JSX 標籤而不包進另一個標籤或 Fragment。
JSX 與 HTML 的差異:4 條規則
React 官方把 JSX 比 HTML 嚴格的地方整理成三條規則,加上元件命名規則,一共四件事:
1. 只能回傳一個根元素
元件要回傳多個元素時,要用單一父標籤包起來。不想多加一層 <div>,可以寫 <> 與 </>,這種空標籤稱為 Fragment,能分組又不在瀏覽器的 HTML 樹裡留下痕跡。
2. 標籤一定要關閉
JSX 要求標籤明確關閉:<img> 要寫成 <img />,<li>oranges 要寫成 <li>oranges</li>。HTML 允許的省略寫法在 JSX 裡會直接造成語法錯誤。
3. 屬性用 camelCase
JSX 轉成 JavaScript 後,屬性會變成物件的鍵,而 JavaScript 的名稱不能含連字號,也不能是 class 這類保留字,所以 React 裡許多 HTML 與 SVG 屬性改用 camelCase:stroke-width 寫成 strokeWidth,class 寫成 className。對照如下:
| HTML | JSX | 備註 |
|---|---|---|
class="photo" | className="photo" | 依對應的 DOM 屬性命名 |
for="name" | htmlFor="name" | React 官方列的標準 DOM 屬性,用於 <label> 與 <output> |
tabindex="1" | tabIndex={1} | 舊版官方文件的例子 |
style="background-color: black" | style={{ backgroundColor: 'black' }} | 行內樣式要傳物件,屬性名稱用 camelCase |
aria-label、data-id | 維持 aria-label、data-id | 官方說明這兩類基於歷史原因維持 HTML 的連字號寫法 |
這些屬性在輸出的 HTML 裡會轉回一般寫法。用 react-dom/server 的 renderToStaticMarkup 輸出:
const React = require('react');
const { renderToStaticMarkup } = require('react-dom/server');
const e = React.createElement;
console.log(renderToStaticMarkup(
e('div', null,
e('label', { htmlFor: 'n', className: 'lbl' }, '名稱'),
e('input', { id: 'n', tabIndex: 1 }),
e('p', { style: { backgroundColor: 'black', color: 'pink' } }, 'x'))
));

圖中 == F. HTML output 是這段腳本的結果:htmlFor 輸出成 for、className 輸出成 class、tabIndex 輸出成 tabindex,style 物件變成 background-color:black;color:pink。屬性大小寫與引號規範可前往〈HTML 大小寫與引號規範:別讓細節成為生產環境的隱形炸彈〉。
把 HTML 的 class 原封不動貼進 JSX,React 會在主控台提醒。同一張圖的 == I. class attr console.error 是對 div 傳入 class 時 console.error 收到的內容:格式字串「Invalid DOM property %s. Did you mean %s?」,參數是 class 與 className。
4. 自訂元件用大寫開頭
React 官方的 createElement 文件說明:JSX 中 <Something /> 等同 createElement(Something),<something />(小寫)則等同 createElement('something'),傳入的是字串,會被當成內建 HTML 標籤。元件教學也寫明,React 元件名稱必須以大寫字母開頭,否則無法運作。
同一張圖的 == H. lowercase component:傳函式本身給 createElement 輸出 <b>hi</b>;傳字串 'hello' 則輸出 <hello></hello>,變成不存在的 HTML 標籤,所以小寫元件名稱不會執行元件函式。
在 JSX 裡放 JavaScript:大括號用法
要在標記裡使用變數、函式呼叫等 JavaScript 運算式,就用大括號 {}。React 官方列出大括號只能用在兩個地方:
- 標籤內的文字:
<h1>{name}'s To Do List</h1>可以,<{tag}>...</{tag}>不行。 - 緊接在
=後面的屬性值:src={avatar}會讀取變數avatar,src="{avatar}"則會傳入字串"{avatar}"。
{{ 和 }} 不是特殊語法,只是大括號裡放了一個 JavaScript 物件,最常見的是行內樣式:
<ul style={{ backgroundColor: 'black', color: 'pink' }}>
<li>Improve the videophone</li>
</ul>
外層大括號是 JSX 的,內層是物件,屬性名稱用 camelCase。
大括號裡可以放字串、數字或陣列這類 React 能顯示的值,但不能把一般物件直接當子節點。同一張輸出圖的 == J. object child 是把 { name: 'Sean' } 當子節點傳給 div 的結果,拋出的錯誤是:
Objects are not valid as a React child (found: object with keys {name}). If you meant to render a collection of children, use an array instead.
官方文件的挑戰題也是這個錯誤,原因是把整個 person 物件放進標籤內容。
JSX 裡常見的語法錯誤
從 HTML 貼進 JSX 最常遇到標籤沒關閉與多個根元素。用 esbuild 轉換三段有問題的 JSX:
const esbuild = require('esbuild');
for (const code of [
'const a = <img src="a.png">;',
'const a = <h1>A</h1><p>B</p>;',
'const a = <li>one;',
]) {
try { esbuild.transformSync(code, { loader: 'jsx' }); }
catch (e) { console.log('INPUT:', code); console.log(e.errors.map(x => 'ERROR: ' + x.text).join('\n')); }
}

三段的輸出:
<img src="a.png">未關閉:Unexpected end of file before a closing "img" tag,指出img沒有關閉。<h1>A</h1><p>B</p>兩個並排的根元素:Unterminated regular expression。esbuild 的訊息沒有直接說「要用單一元素包起來」,遇到這類看不懂的錯誤時,先檢查return後面是不是有多個並排的元素。<li>one;未關閉:Unexpected end of file before a closing "li" tag。
不同工具的錯誤措辭不同,但原因都在於上面四條規則。
JSX 會跳脫內容,但不是所有情況都安全
React 舊版官方文件說明,React DOM 預設會跳脫(escape)嵌入 JSX 的值再渲染,所以把使用者輸入放進大括號是安全的,這有助於防止 XSS。用 renderToStaticMarkup 輸出 '<script>alert(1)</script>' 當作文字子節點,結果是 <p><script>alert(1)</script></p>(見輸出圖的 == G. escape),尖括號被轉成 < 與 >,不會被當成標籤。
這個保護只涵蓋當成文字放進 JSX 的值。需要把 HTML 字串當標記渲染時要用 dangerouslySetInnerHTML,可前往〈React 如何正確渲染 HTML 字串?dangerouslySetInnerHTML 用法與範例〉。
不用 JSX 也能寫 React,但實務上多半用 JSX
createElement 的官方文件說它是 JSX 的替代方式:專案無法使用 JSX 時,可以直接呼叫 createElement(type, props, ...children)。React 官方也說,多數 React 開發者偏好 JSX 的簡潔,多數程式碼庫也使用 JSX。
檔案副檔名方面,TypeScript 要使用 JSX 必須用 .tsx;Next.js 的 page 檔案,官方文件列的副檔名是 .js、.jsx、.tsx。
相關主題:預設 Props 寫法見〈解決 React 元件 defaultProps 錯誤:TypeScript Props 預設參數、HOC 與重構踩坑排查〉;圖片 alt 檢查警告見〈React Console Warning:images jsx-a11y/alt-text 警告原因、解決方法與 img alt 設定規範〉。
參考資料與延伸閱讀
- React 官方:Writing Markup with JSX
- React 官方:JavaScript in JSX with Curly Braces
- React 官方:Your First Component
- React 官方:createElement
- React 官方:Common components(htmlFor 等標準 DOM 屬性)
- React Blog:Introducing the New JSX Transform
- React 舊版文件:Introducing JSX
- Babel:@babel/preset-react
- TypeScript:JSX
- facebook/jsx:JSX Specification
- Next.js:page.js 檔案慣例
- 站內延伸閱讀:〈React 工具鏈規格解析:基於 Vite 的初始化流程、目錄架構與生產環境建構〉、〈React JSX 轉譯與 DOM 結構:Fragment 應用、多態元件 as 模式與層級錯位排查〉、〈React 如何正確渲染 HTML 字串?dangerouslySetInnerHTML 用法與範例〉、〈HTML 大小寫與引號規範:別讓細節成為生產環境的隱形炸彈〉
發佈留言