為什麼 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 時,會直覺地想把 useStateuseEffect 寫進任何函式裡,但這樣做很容易踩雷,因為:

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 類型內容
#1useState(0)儲存狀態 count
#2useState('Hi')儲存狀態 text
#3useEffect(...)儲存副作用執行計劃

🔁 這些記錄是「根據呼叫順序」進行追蹤的,所以只要每次渲染時 Hook 的順序不變,React 就能精準對應資料格子,重新使用正確的狀態。

🔍 為什麼「順序」這麼重要?

因為 React 不靠變數名稱、也不靠變數順序來判斷 hook 是誰,它完全只根據「第幾個 hook 是誰」來配對。

如果你把 Hook 寫在 if 裡或條件式裡,就會造成某些情況下:

  • 第 1 個是 useState
  • 某次渲染變成第 1 個是 useEffect

➡️ 那狀態就會錯配,React 會直接報錯。

步驟 4:回傳 JSX,生成虛擬 DOM

完成 Hook 呼叫與資料記錄後,元件函式的最後一行通常會回傳 JSX:

return <div>{text} - {count}</div>;

React 接收到這段 JSX 結構後,會做以下事:

  1. 轉換成 Virtual DOM(虛擬 DOM)
    • 這是一種 JavaScript 物件的樹狀結構,描述畫面要怎麼顯示
  2. 比對前一次的 Virtual DOM(diff 過程)
    • React 只更新有改變的部分,提高效率
  3. 把結果套用到真實畫面(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 像是在跑「狀態登記流程」:

  1. 開始執行元件:準備 hook 系統
  2. 呼叫第 1 個 hook:在記憶格子 1 儲存 useState 值
  3. 呼叫第 2 個 hook:在記憶格子 2 儲存 useEffect 邏輯
  4. 記下 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 的記錄與更新邏輯

Similar Posts