React useEffect 深度讲解
useEffect 基本概念
useEffect
是 React Hooks 中用于处理副作用的钩子函数,它模拟了类组件中的生命周期方法。
jsx
javascript
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// 副作用逻辑
console.log('组件渲染或更新');
// 清理函数(可选)
return () => {
console.log('清理工作');
};
}, [dependencies]); // 依赖数组
}
依赖数组的详细解析
1. 空依赖数组 []
jsx
scss
useEffect(() => {
// 只在组件挂载时执行一次
console.log('组件挂载');
}, []);
2. 包含依赖的数组
jsx
scss
const [count, setCount] = useState(0);
const [name, setName] = useState('');
useEffect(() => {
// 当 count 或 name 变化时执行
console.log(`Count: ${count}, Name: ${name}`);
}, [count, name]);
3. 无依赖数组
jsx
javascript
useEffect(() => {
// 每次渲染后都会执行
console.log('组件渲染');
});
应该被监听的字段
必须监听的字段
1. State 变量
jsx
scss
const [user, setUser] = useState({ id: '', name: '', age: 0 });
const [items, setItems] = useState([]);
useEffect(() => {
// 当 user 变化时执行
fetchUserData(user.id);
}, [user.id]); // ✅ 正确:监听 state
useEffect(() => {
// 当 items 变化时执行
localStorage.setItem('items', JSON.stringify(items));
}, [items]);
2. Props
jsx
scss
function UserProfile({ userId, onUpdate }) {
useEffect(() => {
// 当 userId prop 变化时执行
fetchUserProfile(userId);
}, [userId]); // ✅ 正确:监听 prop
useEffect(() => {
// 当 onUpdate prop 变化时执行(函数引用)
setupUpdateListener(onUpdate);
}, [onUpdate]);
}
3. 由其他 Hooks 返回的值
jsx
scss
const theme = useContext(ThemeContext);
const location = useLocation(); // React Router
const dispatch = useDispatch(); // Redux
useEffect(() => {
// 当 theme 变化时执行
applyTheme(theme);
}, [theme]);
需要谨慎处理的字段
1. 对象和数组
jsx
scss
const [user, setUser] = useState({ name: 'John', age: 25 });
useEffect(() => {
// ❌ 错误:每次渲染都会执行,因为对象引用不同
console.log('User changed');
}, [user]);
// ✅ 正确:监听具体属性
useEffect(() => {
console.log('User name changed');
}, [user.name]);
// ✅ 或者使用 useMemo 优化
const userMemo = useMemo(() => user, [user.name, user.age]);
useEffect(() => {
console.log('User changed');
}, [userMemo]);
2. 函数
jsx
scss
function MyComponent({ onSubmit }) {
const [data, setData] = useState('');
// ❌ 错误:每次渲染都会创建新函数
const handleProcess = () => {
processData(data);
};
useEffect(() => {
handleProcess();
}, [handleProcess]); // 依赖总是变化
// ✅ 正确:使用 useCallback
const handleProcess = useCallback(() => {
processData(data);
}, [data]);
useEffect(() => {
handleProcess();
}, [handleProcess]);
// ✅ 或者将函数定义在 useEffect 内部
useEffect(() => {
const processData = () => {
// 处理数据
};
processData();
}, [data]);
}
除了 state 和 props,其他变量监听的意义
1. 来自 Context 的值
jsx
scss
import { useContext, useEffect } from 'react';
import { AuthContext, ThemeContext } from './context';
function MyComponent() {
const { user } = useContext(AuthContext);
const theme = useContext(ThemeContext);
useEffect(() => {
// 当 Context 值变化时执行
if (user) {
loadUserPreferences(user.id);
}
}, [user]); // ✅ 有意义:监听 Context 值
useEffect(() => {
document.body.className = theme;
}, [theme]);
}
2. URL 参数和路由状态
jsx
scss
import { useParams, useLocation } from 'react-router-dom';
function ProductPage() {
const { productId } = useParams();
const location = useLocation();
useEffect(() => {
// 当 URL 参数变化时执行
fetchProduct(productId);
}, [productId]); // ✅ 有意义:监听路由参数
useEffect(() => {
// 当路由位置变化时执行
trackPageView(location.pathname);
}, [location]);
}
3. 来自 Redux 或其他状态管理的值
jsx
scss
import { useSelector, useDispatch } from 'react-redux';
function Cart() {
const cartItems = useSelector(state => state.cart.items);
const total = useSelector(state => state.cart.total);
useEffect(() => {
// 当 Redux 状态变化时执行
updateCartSummary(cartItems);
}, [cartItems]); // ✅ 有意义:监听 Redux 状态
}
4. 计算值(使用 useMemo)
jsx
scss
function TodoList({ todos, filter }) {
const filteredTodos = useMemo(() => {
return todos.filter(todo =>
todo.text.toLowerCase().includes(filter.toLowerCase())
);
}, [todos, filter]);
useEffect(() => {
// 当计算值变化时执行
console.log('Filtered todos updated:', filteredTodos.length);
}, [filteredTodos]); // ✅ 有意义:监听计算值
}
5. Refs(通常不需要监听)
jsx
scss
function MyComponent() {
const inputRef = useRef(null);
const previousValue = useRef('');
useEffect(() => {
// ❌ 通常不需要监听 ref
// ref 的 .current 属性变化不会触发重新渲染
}, [inputRef]); // 无意义
// ✅ 正确的 ref 使用方式
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []); // 只在挂载时执行
}
6. 外部变量(需要谨慎)
jsx
scss
let externalCounter = 0; // 模块级别的变量
function MyComponent() {
useEffect(() => {
// ❌ 危险:外部变量变化不会触发重新渲染
console.log('External counter:', externalCounter);
}, [externalCounter]); // 依赖不会变化
// ✅ 更好的方式:使用 state 或 ref
const [counter, setCounter] = useState(externalCounter);
useEffect(() => {
const interval = setInterval(() => {
externalCounter++;
setCounter(externalCounter); // 触发重新渲染
}, 1000);
return () => clearInterval(interval);
}, []);
}
最佳实践和常见模式
1. 依赖项完整性
jsx
scss
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
// ✅ 正确:在 effect 内部定义函数
const handleMessage = (msg) => {
showNotification('New message: ' + msg);
};
connection.on('message', handleMessage);
return () => {
connection.disconnect();
};
}, [roomId]); // 所有依赖都声明
}
2. 使用自定义 Hook 封装复杂逻辑
jsx
javascript
// 自定义 Hook
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function fetchData() {
setLoading(true);
try {
const response = await fetch(url);
const result = await response.json();
if (!cancelled) {
setData(result);
}
} catch (error) {
if (!cancelled) {
console.error('Fetch error:', error);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
};
}, [url]); // url 变化时重新获取
return { data, loading };
}
// 使用自定义 Hook
function UserProfile({ userId }) {
const { data: user, loading } = useApi(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
3. 避免无限循环
jsx
scss
function UserProfile() {
const [user, setUser] = useState({ name: '', count: 0 });
// ❌ 危险:可能导致无限循环
useEffect(() => {
setUser(prev => ({ ...prev, count: prev.count + 1 }));
}, [user]); // user 变化触发 effect,effect 又更新 user
// ✅ 正确:只在必要时更新
useEffect(() => {
if (someCondition) {
setUser(prev => ({ ...prev, count: prev.count + 1 }));
}
}, [someCondition]);
}
总结
应该监听的字段:
- State 变量
- Props
- Context 值
- 路由参数和状态
- 来自状态管理的值
- 使用 useMemo 的计算值
监听意义有限的字段:
- Refs(通常不需要)
- 模块级别变量
- 在 effect 内部定义的变量
关键原则:
- 只监听那些可能变化并影响副作用的变量
- 保持依赖数组的完整性
- 对于对象和数组,考虑监听具体属性或使用 useMemo
- 对于函数,使用 useCallback 或将其定义在 effect 内部
- 使用自定义 Hook 封装复杂的效果逻辑
通过合理使用依赖数组,可以确保 useEffect 在正确的时机执行,避免不必要的重复执行和内存泄漏。