useEffect #
一、useEffect概述 #
1.1 什么是副作用 #
副作用是指函数组件中与渲染无关的操作:
text
副作用类型
├── 数据获取 API 请求
├── 订阅 事件监听
├── 定时器 setTimeout/setInterval
├── DOM 操作 直接操作 DOM
└── 存储 localStorage/sessionStorage
1.2 基本语法 #
jsx
useEffect(effectFunction, dependencies);
| 参数 | 说明 |
|---|---|
| effectFunction | 副作用函数 |
| dependencies | 依赖数组(可选) |
二、基本用法 #
2.1 每次渲染后执行 #
jsx
import { useEffect, useState } from 'preact/hooks';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('组件渲染后执行');
});
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
2.2 仅挂载时执行 #
jsx
function Example() {
useEffect(() => {
console.log('组件挂载时执行一次');
}, []); // 空依赖数组
return <div>Example</div>;
}
2.3 依赖变化时执行 #
jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
console.log('userId 变化:', userId);
fetchUser(userId).then(setUser);
}, [userId]); // userId 变化时执行
return <div>{user?.name}</div>;
}
三、清理副作用 #
3.1 清理函数 #
jsx
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// 返回清理函数
return () => {
clearInterval(interval);
console.log('定时器已清理');
};
}, []);
return <div>Seconds: {seconds}</div>;
}
3.2 清理时机 #
jsx
function Example() {
useEffect(() => {
console.log('副作用执行');
return () => {
console.log('清理函数执行');
};
});
return <div>Example</div>;
}
// 执行顺序:
// 1. 副作用执行
// 2. 组件更新时:先执行清理 → 再执行副作用
// 3. 组件卸载时:执行清理
3.3 清理订阅 #
jsx
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = connectToRoom(roomId);
connection.on('message', (msg) => {
console.log('New message:', msg);
});
return () => {
connection.disconnect();
};
}, [roomId]);
return <div>Room: {roomId}</div>;
}
四、数据获取 #
4.1 基本数据获取 #
jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUser() {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch');
}
const data = await response.json();
setUser(data);
setError(null);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <div>{user.name}</div>;
}
4.2 取消请求 #
jsx
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
const controller = new AbortController();
async function search() {
try {
const response = await fetch(
`/api/search?q=${query}`,
{ signal: controller.signal }
);
const data = await response.json();
setResults(data);
} catch (e) {
if (e.name !== 'AbortError') {
console.error(e);
}
}
}
if (query) {
search();
}
return () => {
controller.abort();
};
}, [query]);
return (
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
4.3 防止竞态条件 #
jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!cancelled) {
setUser(data);
}
}
fetchUser();
return () => {
cancelled = true;
};
}, [userId]);
return <div>{user?.name}</div>;
}
五、事件监听 #
5.1 窗口事件 #
jsx
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<div>
{size.width} x {size.height}
</div>
);
}
5.2 键盘事件 #
jsx
function KeyboardShortcuts() {
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
console.log('Escape pressed');
}
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
console.log('Save triggered');
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, []);
return <div>Press keys to see shortcuts</div>;
}
5.3 自定义事件 #
jsx
function EventEmitter() {
useEffect(() => {
const handleCustomEvent = (e) => {
console.log('Custom event:', e.detail);
};
document.addEventListener('myEvent', handleCustomEvent);
return () => {
document.removeEventListener('myEvent', handleCustomEvent);
};
}, []);
const emitEvent = () => {
document.dispatchEvent(new CustomEvent('myEvent', {
detail: { message: 'Hello' }
}));
};
return <button onClick={emitEvent}>Emit Event</button>;
}
六、定时器 #
6.1 setInterval #
jsx
function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => {
setTime(new Date());
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
return <div>{time.toLocaleTimeString()}</div>;
}
6.2 setTimeout #
jsx
function Notification({ message, duration = 3000, onClose }) {
const [visible, setVisible] = useState(true);
useEffect(() => {
const timer = setTimeout(() => {
setVisible(false);
onClose?.();
}, duration);
return () => {
clearTimeout(timer);
};
}, [duration, onClose]);
if (!visible) return null;
return (
<div class="notification">
{message}
</div>
);
}
6.3 自定义 useInterval Hook #
jsx
function useInterval(callback, delay) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => {
savedCallback.current();
}, delay);
return () => clearInterval(id);
}, [delay]);
}
// 使用
function Counter() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(c => c + 1);
}, 1000);
return <h1>{count}</h1>;
}
七、DOM 操作 #
7.1 直接 DOM 操作 #
jsx
function FocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
7.2 测量 DOM #
jsx
function MeasureElement() {
const ref = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
if (ref.current) {
const { width, height } = ref.current.getBoundingClientRect();
setDimensions({ width, height });
}
}, []);
return (
<div>
<div ref={ref} style={{ width: '200px', height: '100px', background: '#f0f0f0' }}>
Content
</div>
<p>Width: {dimensions.width}, Height: {dimensions.height}</p>
</div>
);
}
7.3 动态样式 #
jsx
function DynamicStyle({ isHighlighted }) {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
ref.current.style.backgroundColor = isHighlighted ? 'yellow' : 'white';
}
}, [isHighlighted]);
return <div ref={ref}>Content</div>;
}
八、本地存储 #
8.1 localStorage #
jsx
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// 使用
function TodoApp() {
const [todos, setTodos] = useLocalStorage('todos', []);
return (
<div>
{todos.map(todo => (
<div key={todo.id}>{todo.text}</div>
))}
</div>
);
}
8.2 sessionStorage #
jsx
function useSessionStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = sessionStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
sessionStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
九、依赖数组 #
9.1 依赖规则 #
jsx
// 无依赖:每次渲染都执行
useEffect(() => {
console.log('每次渲染');
});
// 空数组:仅挂载时执行
useEffect(() => {
console.log('仅挂载时');
}, []);
// 有依赖:依赖变化时执行
useEffect(() => {
console.log('count 变化:', count);
}, [count]);
// 多个依赖
useEffect(() => {
console.log('a 或 b 变化');
}, [a, b]);
9.2 常见错误 #
jsx
function Example({ userId }) {
const [data, setData] = useState(null);
// 错误:缺少依赖
useEffect(() => {
fetchUser(userId).then(setData);
}, []); // userId 变化时不会重新获取
// 正确:包含所有依赖
useEffect(() => {
fetchUser(userId).then(setData);
}, [userId]);
}
9.3 ESLint 规则 #
javascript
// .eslintrc.js
{
"rules": {
"react-hooks/exhaustive-deps": "warn"
}
}
十、useLayoutEffect #
10.1 与 useEffect 的区别 #
| Hook | 执行时机 | 用途 |
|---|---|---|
| useEffect | 渲染后异步执行 | 大多数副作用 |
| useLayoutEffect | DOM 更新后同步执行 | DOM 测量/操作 |
10.2 使用场景 #
jsx
import { useLayoutEffect, useRef, useState } from 'preact/hooks';
function Tooltip({ content }) {
const ref = useRef(null);
const [position, setPosition] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setPosition({ top: -height - 10, left: 0 });
}, [content]);
return (
<div
ref={ref}
style={{ position: 'absolute', top: position.top, left: position.left }}
>
{content}
</div>
);
}
十一、最佳实践 #
11.1 分离关注点 #
jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });
// 用户数据获取
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// 窗口大小监听
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>{/* ... */}</div>;
}
11.2 提取自定义 Hook #
jsx
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// 使用
function ResponsiveComponent() {
const { width } = useWindowSize();
return (
<div>
{width < 768 ? <MobileView /> : <DesktopView />}
</div>
);
}
十二、总结 #
| 要点 | 说明 |
|---|---|
| 执行时机 | 渲染后执行 |
| 清理函数 | 返回函数,卸载或更新前执行 |
| 依赖数组 | 控制执行时机 |
| 数据获取 | 注意取消请求和竞态条件 |
| 事件监听 | 记得清理 |
核心原则:
- 正确设置依赖数组
- 清理副作用避免内存泄漏
- 分离不同关注点
- 提取可复用逻辑为自定义 Hook
最后更新:2026-03-28