為什麼 React 在「沒資料時」也要先渲染?——揭開 useEffect 與元件渲染時機的真相
更新日期: 2025 年 4 月 18 日
你是否在學習 React 時,有過這樣的困惑?
- 我明明要等 API 回傳資料再顯示,為什麼 React 一開始就先 render 一次?
- 明明沒資料,畫面先空著不奇怪嗎?
useEffect
執行時不是應該是 componentDidMount 階段嗎?那不就是 render 完了才抓資料?
別急,這篇文章就帶你完整釐清 React 的渲染邏輯與生命週期,理解為什麼這樣的設計,反而讓 React 更強大!
React 的核心哲學:UI = f(state)
React 的設計理念可以用一個數學公式來概括:
UI = f(state)
也就是說:使用者介面(UI)是狀態(state)的函數。
這代表什麼呢?
在 React 裡,你不需要手動告訴電腦何時該更新畫面,你只要做兩件事:
- 定義 state 是什麼
- 描述當 state 改變時,畫面要怎麼「自動」改變
React 會根據你宣告的狀態(如:空資料、有資料、錯誤等等),自動重新 render 出符合該狀態的畫面,這就是 宣告式程式設計(Declarative Programming)。
傳統的命令式程式設計(Imperative Programming)
傳統的命令式寫法,你可能會這樣寫邏輯:
if (資料還沒來) {
顯示 loading 畫面
} else if (有錯誤) {
顯示錯誤畫面
} else {
把資料放進畫面上
}
你得自己去判斷「何時該顯示什麼」、「畫面哪裡該改」,很容易漏掉某個狀態,或造成程式難以維護。
而在 React 裡,你只需要說明「當 state 是這樣時,畫面長這樣」,不需要自己控制畫面更新邏輯,因為 React 幫你處理了所有畫面更新的時機點。
為什麼資料還沒來,也要「先 render」?
我們來看一段實際例子:
function MemberList() {
const [members, setMembers] = useState([]);
useEffect(() => {
fetch('/api/members')
.then(res => res.json())
.then(data => setMembers(data));
}, []);
return (
<ul>
{members.map(member => (
<li key={member.id}>{member.name}</li>
))}
</ul>
);
}
🪜 React 背後的實際執行流程是這樣的:
步驟 | 發生的事 | 狀態值 |
---|---|---|
1️⃣ 初次 render | members 是空陣列 → React 渲染出空的 <ul> | [] |
2️⃣ 掛載完成 | useEffect 執行,開始發送 API 請求 | [] |
3️⃣ 資料回來 | 呼叫 setMembers(data),更新狀態 | [member1, member2, ...] |
4️⃣ 更新 render | React 再次渲染畫面,顯示資料列表 | [member1, member2, ...] |
你可能會問:
❓ 為什麼不「等資料回來後」再 render?
因為 React 設計上就是「每一種狀態都會被渲染出對應的畫面」,即使現在狀態是「還沒資料」,也能對應一個適當的畫面(例如:顯示 loading 中)。
這不但讓程式更模組化,也讓使用者體驗更流暢!
為什麼「沒資料也要渲染」是必要的?
提供立即回饋,改善使用者體驗
想像一下使用者點開頁面,畫面一片空白,會覺得程式壞了嗎?
這時你可以這樣寫:
if (members.length === 0) return <p>資料載入中...</p>;
讓使用者知道「資料正在載入中」,這是一種良好的 UX 實踐。
每一種 state 都有「對應的畫面」
你可以這樣思考:
狀態 | 對應畫面 |
---|---|
data = null | 顯示載入中 |
data.length === 0 | 顯示「查無資料」 |
data.length > 0 | 顯示資料列表 |
這種「狀態驅動 UI」的設計,讓你的元件更容易測試、更容易維護,也更接近使用者實際看到的畫面行為。
支援元件初始化即 render 的邏輯(懶加載、動畫、錯誤處理等)
很多 UI 行為其實在資料還沒到之前就要執行,例如:
- 懶加載的動畫骨架(skeleton)
- 圖表元件初始化動畫
- 空資料時的提示畫面
- 錯誤重試按鈕…
這些 UI 元素都需要「沒資料」的狀態就能 render,而不是等資料到才 render。
小結
React 是「狀態驅動的 UI 框架」,資料是否準備好,本身就是一種 state。
不管資料有沒有回來,React 都能依照目前的 state 先渲染對應的畫面,這才是它的強大之處。
所以下次你看到「還沒資料也先 render」,別懷疑——這不是 bug,而是 React 的特色與設計哲學。
元件渲染時機 vs. useEffect 執行時機
當你開始在 React 中撰寫 API 請求、動畫效果或訂閱行為時,常常會接觸到 useEffect
。
但它什麼時候執行?會不會影響畫面?與元件的渲染順序有什麼關係?這一段我們一次搞懂!
掛載階段(Mounting Phase):初次 render 的兩個階段
當元件第一次出現在畫面上,React 會經歷兩個步驟:
初次 render:
React 根據當前的 state(例如空陣列、null)去建立 Virtual DOM → 計算出一個對應的 UI。
⚠️ 此時 useEffect
還沒執行,畫面已經先出現在瀏覽器上。
完成掛載(commit phase):
當畫面真正顯示出來、DOM 結構已經渲染完畢,React 才會去觸發 useEffect(() => {...}, [])
中的副作用。
這通常包含:
- 資料請求
- DOM 操作(focus、scroll)
- 設定訂閱(websocket、事件監聽)
✅ 所以:useEffect
的執行時機是在 render 之後、畫面顯示完成之後。
這個流程非常像 Class Component 裡的 componentDidMount()
,所以我們常說 useEffect(..., [])
模擬的就是「元件掛載完成時」的副作用。
更新階段(Updating Phase):當 state 改變時
當你呼叫 setState()
或 setMembers(data)
改變狀態時,會進入 更新階段:
- React 偵測到 state 或 props 改變
- 執行新的 render → 建立新的 Virtual DOM
- 對比新舊 DOM(diffing)
- 更新畫面中真正變動的部分(高效!)
✅ 這種重新 render 的行為是 React 的精髓:
- 你不用自己操作 DOM
- 你只需要描述「畫面要長什麼樣」
- React 自動幫你處理更新與重繪
這讓 UI 與資料狀態保持一致,也讓開發更有信心。
如果我不想讓畫面「先閃過空資料再更新」怎麼辦?
React 預設的行為是「先 render 初始狀態 → 資料載入後再更新畫面」,但有時候你不希望畫面這樣閃來閃去。
這時,你可以使用一個額外的 loading
狀態,搭配條件渲染。
const [loading, setLoading] = useState(true);
const [members, setMembers] = useState([]);
useEffect(() => {
fetch("/api/members")
.then(res => res.json())
.then(data => {
setMembers(data);
setLoading(false); // 資料拿到了,關掉 loading 狀態
});
}, []);
if (loading) return <p>資料載入中...</p>; // 或顯示 skeleton 動畫
return (
<ul>
{members.map(member => (
<li key={member.id}>{member.name}</li>
))}
</ul>
);
這樣可以避免「先空畫面 → 突然出現資料」的突兀感,改善使用者體驗。
常見疑問總整理
問題 | 解答 |
---|---|
為什麼 useEffect 是 render 完才執行? | 因為它模擬 componentDidMount,只有在 DOM 完成後才能執行副作用邏輯(例如操作 input、抓資料) |
為什麼沒資料也要先 render? | 因為「沒資料」本身也是一種 state,React 會根據當前狀態先渲染對應畫面(例如 loading) |
這樣 render 兩次,不會很耗效能嗎? | 不會。React 使用 Virtual DOM 做差異比對(diffing),只會更新有變化的部分,效能處理得非常好 |
如何避免畫面閃爍? | 加上 loading 狀態 + 條件渲染,讓畫面在資料回來前只顯示載入提示或 skeleton |
小結
沒資料也 render?不奇怪,這正是 React 的設計之美!
React 要你專注在「描述畫面長怎樣」,而不是「怎麼控制畫面更新」。這樣能讓程式碼更直觀、邏輯更清楚、可維護性更高。