你的 useEffect 為什么“跑了兩遍”?真相在這里
如果你在 React 里寫了一個 useEffect,明明只打算執行一次,結果在控制臺看見它執行了兩次——別擔心,你絕對不是唯一的那個。
你也許問過:
- “這是個 bug 嗎?”
- “我寫法不對?”
- “React 把什么東西弄壞了?”
好消息是: 這不是 bug,而是刻意為之。 下面把來龍去脈講清楚,并給出正確的思考方式。
你大概率寫的是這段代碼
useEffect(() => {
console.log("Effect ran");
}, []);你本來預期它只會打印一次,對吧?
可是在開發環境里,你可能看到:
Effect ran
Effect ran原因:React 的 Strict Mode(嚴格模式)
從 React 18+ 開始,很多新建項目(如 create-react-app、Next.js 等腳手架)在開發模式會默認啟用 StrictMode。
StrictMode 只在開發環境下,會把某些生命周期相關邏輯額外再跑一輪——目的,是幫助你盡早發現不安全的副作用(例如未清理的訂閱、對外部可變對象的意外修改、遺漏的異步清理等)。
<React.StrictMode>
<App />
</React.StrictMode>
?注意:這個“雙觸發”只發生在開發環境。 一旦打包上線到生產,
useEffect會按預期只跑一次。
React 為什么這樣做
React 的意圖,是讓你提前暴露潛在風險,從而在真正上線前就把坑填好。因此,它試圖“加壓測試”你的副作用代碼,以便發現這些問題:
- 忘記清理訂閱或計時器
- 在渲染階段引發副作用(應當避免)
- 重復的拉取請求或多余的狀態重置
把它想象成一次“安全演練”:在進入生產之前,先在訓練場把問題暴露出來,因此導致你看到“多跑了一次”的表象。
實例:數據請求為什么會被發兩次
假設你在 useEffect 中請求數據:
useEffect(() => {
fetch("/api/data")
.then(res => res.json())
.then(setData);
}, []);在開發環境,效果被“演練”兩次,于是會打兩次 API。這當然不理想。
? 應對辦法:加一個“只執行一次”的標記(ref)或采用專門的數據請求庫。
const hasFetched = useRef(false);
useEffect(() => {
if (!hasFetched.current) {
hasFetched.current = true;
fetch("/api/data")
.then(res => res.json())
.then(setData);
}
}, []);或者,直接使用 React Query(等數據層庫),它們在實踐中已經把這類問題處理得相對穩妥。
到底哪些東西會被“觸發兩次”
在 React 18 的嚴格模式下,組件初次掛載時,下列內容會被“演練式”調用兩次:
useEffectuseLayoutEffect- 類組件的
componentDidMount與componentWillUnmount
重要的是要分清:React 并沒有讓你的應用“雙渲染”。 實際流程是:掛載 → 卸載 → 再次掛載,借此驗證你的副作用是否可安全重復。
如何確認“這只是開發時的行為”
最直接的辦法:跑一遍生產構建。
npm run build
npm start在生產模式下,你會看到 useEffect只執行一次。 因此,無需驚慌——這不是系統壞掉,而是 React 在幫你練兵。
總結
一開始,這個行為確實會讓人困惑,甚至會誤以為“哪里炸了”。然而,當你理解了 React 的設計初衷之后,你會發現它其實是在幫你避免那些“還沒寫出來”的 bug。換句話說,雖然看著“麻煩”,但長遠看是增益。
useEffect在開發環境里因 Strict Mode 會顯得“執行兩次”;- 這屬于特性而非缺陷,目的是提前發現副作用問題;
- 生產環境按預期只跑一次,因此上線后行為是正常的。
想降低困惑、提升可控性,可以考慮:
- 理解嚴格模式在測試什么,以及為什么要“掛載—卸載—再掛載”;
- 在確需“一次性邏輯”的地方,用 標記/ref 做顯式防抖;
- 采用 React Query 等數據層工具,把副作用與緩存、并發、重復請求控制交給庫來處理。

























