什么是 useEffect Hook?
useEffect 是 React 中的一个 Hook,用于在函数组件中处理副作用(side effects)。副作用是指那些不在组件渲染过程中直接发生的事情,比如:
- 发起网络请求(比如从服务器获取数据)
- 操作 DOM(比如改变页面标题)
- 设置定时器(比如 setTimeout 或 setInterval)
- 订阅外部数据源(比如 WebSocket)
简单来说,useEffect 让你在组件渲染后(或某些特定时机)执行一些额外的操作。
useEffect 的基本语法
useEffect 是一个函数,接收两个参数:
- 回调函数:定义你要执行的副作用逻辑。
- 依赖数组(可选):控制副作用何时运行。
基本结构如下:
import { useEffect } from 'react';function MyComponent() {useEffect(() => {// 这里写副作用代码console.log('副作用执行了!');}, [/* 依赖项 */]);return <div>我的组件</div>;
}
- 回调函数:每次组件渲染后(或依赖项变化时),这里面的代码会运行。
- 依赖数组:告诉 React 什么时候需要重新运行回调函数。如果不传依赖数组,副作用每次渲染都会运行。
useEffect 的三种运行时机
根据依赖数组的不同,useEffect 的运行时机有以下三种情况:
1. 没有依赖数组
如果不提供依赖数组,副作用会在每次组件渲染后都运行。
useEffect(() => {console.log('组件每次渲染都会运行我!');
});
适用场景:很少用,因为每次渲染都运行副作用可能会导致性能问题。
2. 空依赖数组 []
如果提供一个空的依赖数组,副作用只在组件首次渲染后运行一次,类似于类组件中的 componentDidMount。
useEffect(() => {console.log('我只在组件首次渲染时运行!');
}, []);
适用场景:
- 初始化时获取数据(比如从 API 加载数据)
- 设置一次性的事件监听器
3. 有依赖项的数组 [dep1, dep2, ...]
如果依赖数组中有变量,副作用会在组件首次渲染以及依赖项发生变化时运行。
import { useState, useEffect } from 'react';function MyComponent() {const [count, setCount] = useState(0);useEffect(() => {console.log(`count 变了,现在是 ${count}`);}, [count]);return (<div><p>计数: {count}</p><button onClick={() => setCount(count + 1)}>加1</button></div>);
}
运行逻辑:
- 首次渲染时,useEffect 运行。
- 每次 count 变化时,useEffect 再次运行。
- 如果 count 没变,即使组件重新渲染,useEffect 也不会运行。
适用场景:
- 根据状态或 props 变化执行副作用(比如根据用户 ID 获取用户信息)
useEffect 的清理函数
有时候,副作用需要清理,比如清除定时器、取消网络请求或移除事件监听器。useEffect 的回调函数可以返回一个清理函数,在组件卸载或副作用重新运行前执行。
useEffect(() => {const timer = setInterval(() => {console.log('计时器运行中...');}, 1000);// 返回清理函数return () => {clearInterval(timer);console.log('计时器被清理了!');};
}, []);
清理函数的运行时机:
- 组件卸载时(类似于 componentWillUnmount)。
- 如果有依赖数组,依赖项变化导致副作用重新运行前,清理函数会先执行。
适用场景:
- 清除定时器(setTimeout、setInterval)
- 取消订阅(比如 WebSocket 或事件监听器)
- 防止内存泄漏
常见使用场景
以下是一些 useEffect 的典型例子,帮助你理解它的实际用途。
1. 获取数据(API 请求)
import { useState, useEffect } from 'react';function UserList() {const [users, setUsers] = useState([]);useEffect(() => {fetch('https://api.example.com/users').then(response => response.json()).then(data => setUsers(data));}, []); // 空数组,只在首次渲染时请求return (<ul>{users.map(user => (<li key={user.id}>{user.name}</li>))}</ul>);
}
2. 动态更新页面标题
import { useState, useEffect } from 'react';function Counter() {const [count, setCount] = useState(0);useEffect(() => {document.title = `你点击了 ${count} 次`;}, [count]); // 依赖 count,每次 count 变化更新标题return (<div><p>计数: {count}</p><button onClick={() => setCount(count + 1)}>加1</button></div>);
}
3. 设置事件监听器
import { useEffect } from 'react';function WindowSize() {useEffect(() => {const handleResize = () => {console.log(`窗口大小: ${window.innerWidth} x ${window.innerHeight}`);};window.addEventListener('resize', handleResize);// 清理事件监听器return () => {window.removeEventListener('resize', handleResize);};}, []); // 空数组,只在首次渲染时设置监听器return <div>调整窗口大小试试!</div>;
}
注意事项(新手常见坑)
-
不要在 useEffect 中直接修改状态,可能导致无限循环
useEffect(() => {setCount(count + 1); // 错误!会导致无限渲染 }, [count]);
解决办法:确保状态更新有条件控制,或者使用空依赖数组。
-
依赖数组要包含所有用到的变量 如果 useEffect 里的代码用到了某个状态或 props,但没在依赖数组中声明,可能会导致 bug。ESLint 的 react-hooks/exhaustive-deps 规则会帮你检查。
useEffect(() => {console.log(count); // count 必须在依赖数组中 }, [count]); // 正确
-
清理函数的重要性 如果不清理定时器或事件监听器,可能会导致内存泄漏,尤其是在组件频繁挂载/卸载时。
-
不要把 useEffect 当成普通函数 useEffect 是为副作用设计的,不要用它来处理渲染逻辑(比如直接修改 DOM 或计算值)。渲染逻辑应该放在组件函数体或 useMemo 中。
总结
- useEffect 的作用:处理副作用,比如数据获取、DOM 操作、事件监听等。
- 运行时机:
- 无依赖数组:每次渲染都运行。
- 空数组 []:仅首次渲染运行。
- 有依赖项 [dep1, dep2]:首次渲染 + 依赖项变化时运行。
- 清理函数:返回一个函数来清理副作用,防止内存泄漏。
- 常见场景:API 请求、更新标题、设置定时器或事件监听器。