在 React 的函數組件中,useEffect
Hook 是一個強大且不可或缺的工具。它允許我們處理副作用 (side effects)——那些在組件渲染之外發生的操作。但是,什么時候才是使用 useEffect
的正確時機呢?讓我們深入探討一下!
什么是副作用?
在 React 的世界里,副作用是指任何在組件渲染周期之外與外部世界交互的操作。常見的副作用包括:
- 數據獲取 (Data fetching):從 API 加載數據。
- 訂閱 (Subscriptions):設置事件監聽器或訂閱外部數據源 (如 WebSocket)。
- 手動更改 DOM (Manually changing the DOM):直接操作瀏覽器 DOM (雖然通常應避免,但有時是必要的)。
- 計時器 (Timers):設置
setTimeout
或setInterval
。 - 日志記錄 (Logging):向分析服務發送數據。
useEffect
的目的就是讓你在函數組件中也能夠執行這些副作用操作。
useEffect
的基本語法
JavaScript
import React, { useState, useEffect } from 'react';function MyComponent({ someProp }) {const [data, setData] = useState(null);useEffect(() => {// 副作用代碼在這里執行console.log('Component mounted or someProp updated!');// 比如,基于 someProp 獲取數據fetch(`https://api.example.com/data?id=${someProp}`).then(response => response.json()).then(result => setData(result));// 清理函數 (可選)return () => {console.log('Component unmounted or before next effect runs');// 在這里進行清理,比如取消訂閱、清除計時器等};}, [someProp]); // 依賴項數組return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}
useEffect
接收兩個參數:
- 一個函數:這個函數包含了你的副作用邏輯。
- 一個可選的依賴項數組 (dependency array):這個數組告訴 React 什么時候應該重新運行你的副作用函數。
何時使用 useEffect
?主要場景詳解
1. 組件掛載后執行操作 (ComponentDidMount) 🚀
如果你希望在組件首次渲染到 DOM 后執行某些操作(例如,獲取初始數據、設置事件監聽器),可以將依賴項數組設置為空數組 []
。
JavaScript
useEffect(() => {// 僅在組件掛載后運行一次console.log('Component has mounted!');fetchInitialData();return () => {// 在組件卸載時運行console.log('Component will unmount!');cleanupSubscriptions();};
}, []); // 空依賴項數組
2. 依賴項更新時執行操作 (ComponentDidUpdate) 🔄
當你希望在某個特定的 prop
或 state
發生變化時重新運行副作用,你需要將這些 prop
或 state
添加到依賴項數組中。
JavaScript
const [userId, setUserId] = useState(1);useEffect(() => {// 當 userId 變化時,重新獲取用戶數據console.log(`Fetching data for user: ${userId}`);fetch(`/api/users/${userId}`).then(res => res.json()).then(setData);// 如果需要,這里也可以返回一個清理函數
}, [userId]); // 依賴項:userId
重要提示:如果你在 useEffect
內部使用了外部作用域的變量 (props, state, 或組件內部定義的函數),并且希望在這些變量改變時重新運行 effect,那么務必將它們包含在依賴項數組中。否則,你的 effect 可能會捕獲到過時的(stale)值,導致難以調試的 bug。
3. 組件卸載前執行清理操作 (ComponentWillUnmount) 🧹
useEffect
返回的函數(我們稱之為清理函數 (cleanup function))會在以下兩種情況下執行:
- 組件卸載時:用于清除定時器、取消網絡請求、移除事件監聽器等,防止內存泄漏。
- 在下一次副作用函數運行之前(如果依賴項發生變化導致副作用重新運行)。
JavaScript
useEffect(() => {const timerId = setInterval(() => {console.log('Tick');}, 1000);// 清理函數:在組件卸載或依賴項變化導致 effect 重新運行前執行return () => {clearInterval(timerId);console.log('Timer cleared');};
}, []); // 假設這個 timer 只在掛載時設置
4. 每次渲染后都執行操作 (謹慎使用!) ??
如果省略依賴項數組,useEffect
中的副作用函數會在每次組件渲染之后執行。這通常不是你想要的行為,因為它很容易導致性能問題或無限循環(例如,在 effect 內部更新了某個 state,而這個 state 的更新又觸發了重新渲染)。
JavaScript
useEffect(() => {// 每次渲染后都會執行,除非絕對必要,否則請避免console.log('Component re-rendered');
}); // 沒有依賴項數組
經驗法則:總是嘗試提供依賴項數組。如果你確實需要在每次渲染后運行,請仔細檢查邏輯以避免無限循環。ESLint 的 eslint-plugin-react-hooks
插件中的 exhaustive-deps
規則會幫助你檢查依賴項數組是否完整。
何時不應該使用 useEffect
?
- 用于純計算或數據轉換:如果某個值可以根據現有的
props
或state
直接計算出來,那么不需要useEffect
。直接在組件渲染邏輯中計算即可。 JavaScript// 不好 ? const [fullName, setFullName] = useState(''); useEffect(() => {setFullName(`${firstName} ${lastName}`); }, [firstName, lastName]);// 好 ? const fullName = `${firstName} ${lastName}`;
- 響應用戶事件來轉換數據:對于由用戶事件(如點擊按鈕)直接觸發的數據轉換,應該在事件處理函數中進行,而不是
useEffect
。useEffect
更適合響應props
或state
的 變化 而不是直接的用戶交互。
useEffect
關鍵點:
- 空數組
[]
:僅在掛載和卸載時運行。 - 包含依賴項
[dep1, dep2]
:在掛載時以及任何依賴項發生變化時運行。 - 無數組 (省略):每次渲染后都運行(通常應避免)。
- 清理函數:用于防止內存泄漏和不必要的行為。