為什麼 React 能區分「元件」與「普通函式」?
更新日期: 2025 年 4 月 25 日
學習 React 的過程中,你可能會遇到一個有趣的現象:Hook(像是 useState
, useEffect
)只能用在元件裡,不能用在普通函式中。
例如:
function MyComponent() {
const [isOpen, toggleOpen] = useToggle(); // ✅ 合法,因為是元件
}
function runLogic() {
const [isOpen, toggleOpen] = useToggle(); // ❌ 錯誤,因為是普通函式
}
但 React 是怎麼知道 MyComponent
是元件,而 runLogic
只是一般函式?這篇文章將一步步帶你了解背後的運作邏輯。
React 元件本質上是「函式」
在 React 中,元件本質上就是函式,這點常常讓人困惑。
function Hello() {
return <h1>Hello, world!</h1>;
}
上面這段看起來像函式,但 React 把它當作元件使用。
然而下面這個:
function helper() {
console.log("這只是個工具函式");
}
React 卻不會當作元件處理。為什麼?
React 如何判斷「這是元件」?
React 的判斷方式其實很直覺,主要有以下幾個依據:
名稱首字大寫
在 JSX 中,只要函式名稱是大寫開頭,React 就會把它當成元件使用。
function MyComponent() {
return <div>我是元件</div>;
}
<MyComponent /> // ✅ React 會當成元件來處理
相對地,若是小寫開頭的函式或標籤,就會當成 HTML 標籤或普通函式:
function myComponent() {
return <div>這不會被當成元件</div>;
}
<myComponent /> // ❌ React 會當成原生 DOM 標籤,結果會錯
👉 這是為什麼我們寫 React 元件時,一定要用大寫開頭的原因!
是否被當作 JSX 標籤使用
React 不會主動「掃描所有函式」來看哪個是元件,而是看你在 render 過程中有沒有用 JSX 把它呼叫成一個元件。
<MyComponent /> // JSX 呼叫,React 才會進一步處理並執行這個函式
此時 React 就會:
- 呼叫這個函式(即你定義的元件)
- 在內部建立 Hooks 的追蹤邏輯(Hook Call Stack)
- 檢查有沒有呼叫 useState 等等
Hook 的使用規則:只能在「元件」或「自訂 Hook」中使用
很多初學者在學 React Hook 時,會直覺地想把 useState
或 useEffect
寫進任何函式裡,但這樣做很容易踩雷,因為:
React 的 Hook 系統是「根據函式的呼叫順序」來管理狀態的。
錯誤示範:把 Hook 寫在一般函式中
function runLogic() {
const [value, setValue] = useState(false); // ❌ 錯誤:Invalid Hook call
}
這段會在執行時出錯:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
這是因為這個函式 runLogic()
:
- 沒有被當作 React 元件使用
- 沒有進入 React 的「渲染流程」
- 所以 React 根本不會追蹤這個函式裡發生的事情
正確使用 Hook 的地方
可用 Hook 的範圍 | 範例 | 說明 |
---|---|---|
函式元件(Function Component) | function MyComponent() { ... } | 被 JSX 渲染的元件本體 |
自訂 Hook(Custom Hook) | function useToggle() { ... } | 被元件呼叫,用來封裝邏輯 |
其他 Hook 裡面 | useEffect(() => { useRef(...) }) | 在 Hook 中呼叫另一個 Hook 是合法的 |
React 背後的運作原理
想要完全理解「為什麼 Hook 不能寫在一般函式裡」,我們得先從 React 執行元件的完整流程 開始說起。
你會發現,Hook 其實是建立在一整套「狀態追蹤系統」之上的,而這個系統只在 元件渲染階段 被啟動。
步驟 1:React 遇到 JSX
<MyComponent />
這段 JSX 會轉換成以下語法:
React.createElement(MyComponent)
這行程式碼會讓 React 去執行:
MyComponent();
也就是說,React 會把你寫的函式 MyComponent()
拿來呼叫,並期望它回傳一段 JSX,像這樣:
function MyComponent() {
return <h1>Hello, React</h1>;
}
💡為什麼要呼叫這個函式?
因為 React 是「函式式元件架構」,它把每個元件都當作是一個會回傳畫面結構的函式。
所以:
你寫的東西 | React 的理解方式 |
---|---|
<MyComponent /> | 執行 MyComponent() 拿 JSX |
<Button /> | 執行 Button() 拿 JSX |
<App /> | 執行 App() 拿 JSX |
只要你用 <XX />
這種寫法,React 背後就會呼叫那個函式元件,並拿它 return 的東西來決定畫面該長什麼樣子。
步驟 2:React 呼叫元件函式,準備建立 Hook 執行環境
當 React 執行以下這行程式碼時:
const element = MyComponent();
表面上看起來只是「呼叫一個函式」,但在 React 背後,其實觸發了兩個重要機制:
✅ 1. 啟動 Hook 系統(開啟追蹤模式)
React 此時會進入一種「hook 呼叫追蹤模式」,讓它可以知道:
- 你這次 render 的時候,總共呼叫了幾個 hook?
- 每個 hook 的呼叫順序是什麼?
- 它們要儲存什麼資料(像狀態值、effect 函式)?
你可以想像這像是在做「登記表格」,每次呼叫 hook,React 都會在一張表上寫下:
1. useState(0) → 儲存的值是 0
2. useState('Hi') → 儲存的值是 'Hi'
3. useEffect(...) → 延後執行的副作用
✅ 2. 建立 Hook 記錄區塊(儲存 hook 的資料)
這時 React 也會針對這個元件「開一格 hook 記憶倉庫」,用來記錄:
- 每個 hook 對應的資料(狀態值、ref 值、副作用函式等)
- 下次 render 時要繼續用的資料位置
這是為什麼你能這樣寫:
const [count, setCount] = useState(0); // 第一次會是 0
然後下一次重新渲染時,它還記得「count 是 1」,因為它根據呼叫順序對應記錄格子,永遠知道你要拿的是第 1 格的值。
步驟 3:Hook 呼叫時的「順序追蹤」
看看這個元件:
function MyComponent() {
const [count, setCount] = useState(0); // 第 1 個 hook
const [text, setText] = useState('Hi'); // 第 2 個 hook
useEffect(() => {
console.log('Component did mount');
}, []); // 第 3 個 hook
return <div>{text} - {count}</div>;
}
🧾 React 在背後會這樣紀錄:
呼叫順序 | Hook 類型 | 內容 |
---|---|---|
#1 | useState(0) | 儲存狀態 count |
#2 | useState('Hi') | 儲存狀態 text |
#3 | useEffect(...) | 儲存副作用執行計劃 |
🔁 這些記錄是「根據呼叫順序」進行追蹤的,所以只要每次渲染時 Hook 的順序不變,React 就能精準對應資料格子,重新使用正確的狀態。
🔍 為什麼「順序」這麼重要?
因為 React 不靠變數名稱、也不靠變數順序來判斷 hook 是誰,它完全只根據「第幾個 hook 是誰」來配對。
如果你把 Hook 寫在 if
裡或條件式裡,就會造成某些情況下:
- 第 1 個是
useState
- 某次渲染變成第 1 個是
useEffect
➡️ 那狀態就會錯配,React 會直接報錯。
步驟 4:回傳 JSX,生成虛擬 DOM
完成 Hook 呼叫與資料記錄後,元件函式的最後一行通常會回傳 JSX:
return <div>{text} - {count}</div>;
React 接收到這段 JSX 結構後,會做以下事:
- 轉換成 Virtual DOM(虛擬 DOM)
- 這是一種 JavaScript 物件的樹狀結構,描述畫面要怎麼顯示
- 比對前一次的 Virtual DOM(diff 過程)
- React 只更新有改變的部分,提高效率
- 把結果套用到真實畫面(DOM)
非常棒的問題,這是 React Hook 理解的關鍵地雷區之一。我幫你把這段內容擴寫得更完整、更容易理解,包含具體錯誤原因、對比元件流程、以及初心者常見的誤區與心法:
為什麼「普通函式」不行?
當你寫下這樣的程式碼時:
function runLogic() {
const [value, setValue] = useState(false); // ❌ 非法用法
}
這段在執行時會直接報錯,出現如下錯誤訊息:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
為什麼會報這個錯?
因為你違反了 React Hook 的根本使用規則:
✅ Hook 只能被用在「React 正在渲染的元件」或「其他合法的 Hook」中。
runLogic()
雖然也是個函式,但它不是元件,也沒有經過 React 的渲染流程,這就導致 React 根本不知道你在幹嘛,也無法幫你記住任何 Hook 狀態。
🧩 React 在處理 Hook 時,會做哪些事?
要理解錯誤背後的根源,我們可以想像 React 處理 Hook 像是在跑「狀態登記流程」:
- 開始執行元件:準備 hook 系統
- 呼叫第 1 個 hook:在記憶格子 1 儲存 useState 值
- 呼叫第 2 個 hook:在記憶格子 2 儲存 useEffect 邏輯
- 記下 hook 順序、值、關聯 → 建立資料配對表
這一整套登記流程,只有當 React 正式「渲染一個元件」時才會發生。
對比:「元件」 vs 「普通函式」處理流程
項目 | 元件(MyComponent) | 普通函式(runLogic) |
---|---|---|
是否由 React 呼叫? | ✅ 是 | ❌ 不是 |
是否進入 render 階段? | ✅ 會 | ❌ 不會 |
React 是否開啟 Hook 記錄模式? | ✅ 是 | ❌ 否 |
useState 等 hook 是否被追蹤? | ✅ 有狀態記錄 | ❌ 無法追蹤,資料無效 |
是否可以使用 Hook? | ✅ 合法 | ❌ 非法,報錯 |
🧠 換句話說:
React 根本不認識這個函式。
它不是被當成元件使用,也不是被其他 hook 呼叫,
所以你寫在裡面的
useState
對 React 來說根本「不存在」。
這就像你在一場沒有登記的比賽上場一樣,裁判會直接叫你退場:
「你沒在我的名單上,不能比賽!」👮♀️
小結:React 區分元件與函式的核心觀念
判斷方式 | 說明 |
---|---|
名稱是否大寫 | JSX 中大寫開頭會被當成元件處理 |
JSX 是否使用 | React 只有在 render 過程遇到 JSX 標籤時,才會執行那個元件 |
Hook 是否使用 | 只能在 React 元件或自訂 Hook 裡使用 |
React 是否追蹤 | 普通函式 React 不追蹤 → 無法建立 Hook 的記錄與更新邏輯 |