React ref使用注意事项
React 事件监听器中避免使用 state,应使用 ref
在 React 组件中,特别是在事件监听器和副作用外部,应该避免直接使用 state,而应该使用 ref。
为什么使用 ref 而不是 state
- 事件监听器中 state 闭包问题
- 事件监听器(如
addEventListener)会捕获创建时的 state 值 - 每次 state 更新,事件监听器仍然引用旧的 state 值,导致闭包问题
- ref 是可变的对象引用,始终保持最新值
- 事件监听器(如
// 错误示例 - 使用 state
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const handleClick = () => {
console.log(count); // 始终是初始值 0
};
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, []); // 依赖数组为空,handleClick 捕获了初始的 count 值
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
// 正确示例 - 使用 ref
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
// 确保 ref 始终反映最新的 state
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const handleClick = () => {
console.log(countRef.current); // 获取最新值
};
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, []);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}组件卸载的清理操作
处理组件卸载的清理操作时,需要注意清理可能在组件初始化过程中的任何时刻发生。
清理操作的时机协调
- 清理函数可能在组件初始化过程中被调用(如快速切换路由)
- 需要确保初始化操作和清理操作正确协调
function DatabaseComponent() {
const dbRef = useRef(null);
const initializingRef = useRef(false);
const initializedRef = useRef(false);
useEffect(() => {
// 标记开始初始化
initializingRef.current = true;
// 异步初始化数据库
const initDb = async () => {
const db = await connectToDatabase();
dbRef.current = db;
initializedRef.current = true;
initializingRef.current = false;
console.log('数据库连接成功');
};
initDb();
// 清理函数
return () => {
// 如果已初始化,则关闭数据库
if (initializedRef.current && dbRef.current) {
dbRef.current.close();
console.log('数据库连接已关闭');
} else if (initializingRef.current) {
// 如果仍在初始化中,设置一个标记或使用其他逻辑
// 可以通过一个取消信号或其他机制来处理
console.log('在初始化过程中取消');
}
};
}, []);
return <div>数据库组件</div>;
}