一、useEffect基礎概念
1、什么是副作用(Side Effects)?
在React中,副作用是指那些與組件渲染結果無關的操作,例如:
- 數據獲取(API調用)
- 手動修改DOM
- 設置訂閱或定時器
- 記錄日志
2、useEffect的基本語法
import { useEffect } from 'react';function MyComponent() {useEffect(() => {// 副作用邏輯在這里執行return () => {// 清理函數(可選)};}, [dependency1, dependency2]); // 依賴數組(可選)
}
二、useEffect的三種適用方式
1、每次渲染后都執行
useEffect(() => {// 每次組件渲染后都會執行console.log('組件已渲染或更新');
});
2、僅在掛載時執行一次
useEffect(() => {// 只在組件掛載時執行一次console.log('組件已掛載');return () => {// 清理函數,在組件卸載時執行console.log('組件即將卸載');};
}, []); // 空依賴數組
3、依賴特定值變化時執行
useEffect(() => {// 當 count 或 name 變化時執行console.log(`Count: ${count}, Name: ${name}`);return () => {// 清理上一次的 effectconsole.log('清理上一次的 effect');};
}, [count, name]); // 依賴數組
三、useEffect執行機制詳解
四、常見使用場景
1、數據獲取
import { useState, useEffect } from 'react';function UserProfile({ userId }) {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {// 重置狀態setLoading(true);setError(null);const fetchUser = async () => {try {const response = await fetch(`/api/users/${userId}`);const userData = await response.json();setUser(userData);} catch (err) {setError(err.message);} finally {setLoading(false);}};fetchUser();// 不需要清理函數,因為 fetch 會自動取消}, [userId]); // 當 userId 變化時重新獲取if (loading) return <div>加載中...</div>;if (error) return <div>錯誤: {error}</div>;return (<div><h1>{user.name}</h1><p>{user.email}</p></div>);
}
2、事件監聽器
function WindowSizeTracker() {const [windowSize, setWindowSize] = useState({width: window.innerWidth,height: window.innerHeight});useEffect(() => {const handleResize = () => {setWindowSize({width: window.innerWidth,height: window.innerHeight});};// 添加事件監聽window.addEventListener('resize', handleResize);// 清理函數:移除事件監聽return () => {window.removeEventListener('resize', handleResize);};}, []); // 空數組表示只在掛載/卸載時執行return (<div>窗口尺寸: {windowSize.width} x {windowSize.height}</div>);
}
3、定時器
function Timer() {const [seconds, setSeconds] = useState(0);useEffect(() => {const intervalId = setInterval(() => {setSeconds(prevSeconds => prevSeconds + 1);}, 1000);// 清理函數:清除定時器return () => {clearInterval(intervalId);};}, []); // 空依賴數組,只在掛載時設置定時器return <div>已運行: {seconds} 秒</div>;
}
4、手動操作DOM
function FocusInput() {const inputRef = useRef(null);useEffect(() => {// 組件掛載后自動聚焦輸入框if (inputRef.current) {inputRef.current.focus();}}, []); // 空數組表示只在掛載時執行return <input ref={inputRef} placeholder="自動聚焦" />;
}
五、依賴數組的詳細說明
1、依賴數組的規則
// ? 正確:包含所有依賴
useEffect(() => {document.title = `${title} - ${count} 次點擊`;
}, [title, count]); // 所有依賴都聲明// ? 錯誤:缺少依賴
useEffect(() => {document.title = `${title} - ${count} 次點擊`;
}, [title]); // 缺少 count 依賴// ? 正確:使用函數式更新避免依賴
useEffect(() => {const timer = setInterval(() => {setCount(prevCount => prevCount + 1); // 不需要 count 依賴}, 1000);return () => clearInterval(timer);
}, []); // 空依賴數組
2、處理對象和函數依賴
function UserProfile({ user }) {// 使用 useMemo 記憶化對象const userStatus = useMemo(() => ({isActive: user.active,statusText: user.active ? '活躍' : '非活躍'}), [user.active]); // 只有當 user.active 變化時重新計算// 使用 useCallback 記憶化函數const updateUser = useCallback((updates) => {// 更新用戶邏輯}, [user.id]); // 依賴 user.iduseEffect(() => {// 使用記憶化的值和函數console.log(userStatus);updateUser({ lastLogin: new Date() });}, [userStatus, updateUser]); // 依賴記憶化的值return <div>用戶狀態: {userStatus.statusText}</div>;
}
六、useEffect的進階用法
1、多個useEffect的使用
function ComplexComponent({ userId, autoRefresh }) {const [user, setUser] = useState(null);const [notifications, setNotifications] = useState([]);// 獲取用戶數據useEffect(() => {fetchUser(userId).then(setUser);}, [userId]);// 獲取通知(依賴用戶數據)useEffect(() => {if (user) {fetchNotifications(user.id).then(setNotifications);}}, [user]); // 依賴 user// 自動刷新通知useEffect(() => {if (!autoRefresh || !user) return;const intervalId = setInterval(() => {fetchNotifications(user.id).then(setNotifications);}, 30000);return () => clearInterval(intervalId);}, [autoRefresh, user]); // 依賴 autoRefresh 和 userreturn (<div>{/* 渲染邏輯 */}</div>);
}
2、適用自定義Hook封裝useEffect
// 自定義 Hook:使用防抖的搜索
function useDebounce(value, delay) {const [debouncedValue, setDebouncedValue] = useState(value);useEffect(() => {const handler = setTimeout(() => {setDebouncedValue(value);}, delay);return () => {clearTimeout(handler);};}, [value, delay]); // 依賴 value 和 delayreturn debouncedValue;
}// 在組件中使用
function SearchComponent() {const [query, setQuery] = useState('');const [results, setResults] = useState([]);const debouncedQuery = useDebounce(query, 500);useEffect(() => {if (debouncedQuery) {searchAPI(debouncedQuery).then(setResults);} else {setResults([]);}}, [debouncedQuery]); // 依賴防抖后的查詢return (<div><inputvalue={query}onChange={(e) => setQuery(e.target.value)}placeholder="搜索..."/><ul>{results.map(result => (<li key={result.id}>{result.name}</li>))}</ul></div>);
}
七、常見問題與解決方案
1、無限循環問題
// ? 錯誤:導致無限循環
const [count, setCount] = useState(0);useEffect(() => {setCount(count + 1); // 每次渲染都會更新 count,觸發重新渲染
}, [count]); // 依賴 count// ? 正確:使用函數式更新或無依賴
useEffect(() => {setCount(prevCount => prevCount + 1); // 不依賴外部 count 值
}, []); // 空依賴數組
2、異步操作處理
function AsyncComponent() {const [data, setData] = useState(null);useEffect(() => {let isMounted = true; // 跟蹤組件是否掛載const fetchData = async () => {try {const result = await fetch('/api/data');const jsonData = await result.json();if (isMounted) {setData(jsonData); // 只在組件仍掛載時更新狀態}} catch (error) {if (isMounted) {console.error('獲取數據失敗:', error);}}};fetchData();return () => {isMounted = false; // 組件卸載時設置為 false};}, []);return <div>{data ? data.message : '加載中...'}</div>;
}
3、依賴函數的問題
function ProblematicComponent() {const [count, setCount] = useState(0);const logCount = () => {console.log('當前計數:', count);};// ? 問題:logCount 在每次渲染都是新函數useEffect(() => {logCount();}, [logCount]); // 導致每次渲染都執行// ? 解決方案1:將函數移到 useEffect 內部useEffect(() => {const logCount = () => {console.log('當前計數:', count);};logCount();}, [count]); // 只依賴 count// ? 解決方案2:使用 useCallback 記憶化函數const logCountMemoized = useCallback(() => {console.log('當前計數:', count);}, [count]); // 依賴 countuseEffect(() => {logCountMemoized();}, [logCountMemoized]); // 依賴記憶化的函數return <button onClick={() => setCount(c => c + 1)}>增加</button>;
}
八、性能優化技巧
1、條件執行Effect
function ExpensiveComponent({ data, shouldProcess }) {useEffect(() => {if (shouldProcess) {// 只有 shouldProcess 為 true 時才執行昂貴操作performExpensiveOperation(data);}}, [data, shouldProcess]); // 仍然聲明所有依賴
});
2、使用useMemo優化依賴
function OptimizedComponent({ items, filter }) {// 使用 useMemo 避免不必要的重新計算const filteredItems = useMemo(() => {return items.filter(item => item.includes(filter));}, [items, filter]); // 只有當 items 或 filter 變化時重新計算// effect 只依賴記憶化的值useEffect(() => {console.log('過濾后的項目:', filteredItems);}, [filteredItems]); // 依賴記憶化的數組return (<ul>{filteredItems.map(item => (<li key={item}>{item}</li>))}</ul>);
}
3、避免不必要的Effect
// ? 不必要的 effect:可以在渲染期間直接計算
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');useEffect(() => {setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);// ? 更好的方案:在渲染期間直接計算
const fullName = `${firstName} ${lastName}`;
九、最佳實踐總結
1、明確依賴: 始終聲明所有effect中使用的依賴項
2、適當清理: 對于訂閱、定時器等,一定要提供清理函數
3、分離關注點: 使用多個useEffect分離不同的邏輯
4、避免無限循環: 謹慎設置狀態,避免創建渲染循環
5、性能優化: 使用useMemo和useCallback優化依賴項
6、條件執行: 在effect內部添加條件判斷,避免不必要的執行
7、異步處理: 正確處理異步操作的清理和競態條件
總結
useEffect 是React函數組件的核心Hook,它使得副作用管理變得更加聲明式和可預測。通過理解其執行機制、正確使用依賴數組、實現適當的清理邏輯,你可以編寫出高效、可靠的React組件。
記住,useEffect 的核心思想是將副作用與渲染邏輯分離,讓組件更專注于渲染UI,而將副作用操作放在統一的地方管理。這種分離使得代碼更容易理解、測試和維護。