1. 基本概念
useRef 是 React 的一个 Hook,返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数。这个对象在组件的整个生命周期内保持不变。
2. 主要用途和特性
2.1 获取 DOM 元素实例
function TextInputWithFocusButton() {const inputEl = useRef(null);const onButtonClick = () => {// 直接访问 DOM 元素inputEl.current.focus();};return (<><input ref={inputEl} type="text" /><button onClick={onButtonClick}>聚焦输入框</button></>);
}
2.2 存储组件渲染周期之间的共享数据
- useRef 只会在组件初始化时执行一次
- state 改变引起的重新渲染不会导致 useRef 重新执行
- 适合存储不需要触发视图更新的数据
function Counter() {const [count, setCount] = useState(0);const renderCount = useRef(0); // 用于记录渲染次数useEffect(() => {renderCount.current += 1;console.log(`组件已渲染 ${renderCount.current} 次`);});return (<div><p>当前计数: {count}</p><button onClick={() => setCount(count + 1)}>增加</button></div>);
}
2.3 useRef 的重要特性
- current 值的修改不会触发重新渲染
function Example() {const countRef = useRef(0);const handleClick = () => {// 修改 ref 不会导致组件重新渲染countRef.current += 1;console.log('当前值:', countRef.current);};return <button onClick={handleClick}>点击</button>;
}
- 不应作为其他 Hooks 的依赖项
function BadExample() {const valueRef = useRef(0);// ❌ 错误示例useEffect(() => {console.log(valueRef.current);}, [valueRef.current]); // 不要这样做
}function GoodExample() {const valueRef = useRef(0);// ✅ 正确示例useEffect(() => {console.log(valueRef.current);}); // 不将 ref 作为依赖项
}
3. forwardRef 和 useImperativeHandle
3.1 基本用法示例
// CustomInput.jsx
import React, { forwardRef, useImperativeHandle, useRef } from 'react';const CustomInput = forwardRef((props, ref) => {const inputRef = useRef();useImperativeHandle(ref, () => ({// 只暴露需要的方法focus: () => {inputRef.current.focus();},getValue: () => {return inputRef.current.value;}}));return <input ref={inputRef} {...props} />;
});// Parent.jsx
function Parent() {const inputRef = useRef();const handleClick = () => {inputRef.current.focus();console.log(inputRef.current.getValue());};return (<div><CustomInput ref={inputRef} /><button onClick={handleClick}>操作输入框</button></div>);
}
3.2 复杂组件示例(不同粒度的暴露)
const ComplexComponent = forwardRef((props, ref) => {const inputRef = useRef();const checkboxRef = useRef();const formRef = useRef();useImperativeHandle(ref, () => ({// 粒度级别 1:表单级操作form: {reset: () => {inputRef.current.value = '';checkboxRef.current.checked = false;},validate: () => {return inputRef.current.value.length > 0;}},// 粒度级别 2:具体输入框操作input: {focus: () => inputRef.current.focus(),getValue: () => inputRef.current.value,setValue: (value) => {inputRef.current.value = value;}},// 粒度级别 3:简单方法clear: () => {inputRef.current.value = '';}}));return (<form ref={formRef}><input ref={inputRef} type="text" /><input ref={checkboxRef} type="checkbox" /></form>);
});// 使用示例
function ComplexParent() {const componentRef = useRef();const handleOperations = () => {// 使用不同粒度的操作componentRef.current.form.reset();componentRef.current.input.focus();componentRef.current.input.setValue('新值');componentRef.current.clear();if (componentRef.current.form.validate()) {console.log('表单验证通过');}};return (<div><ComplexComponent ref={componentRef} /><button onClick={handleOperations}>执行操作</button></div>);
}
4. 注意事项
- useRef 不能直接引用函数式组件,必须配合 forwardRef 使用
- useRef 的值改变不会触发重新渲染,如果需要在值改变时重新渲染,应使用 useState
- 使用 useImperativeHandle 时,应该只暴露必要的方法,保持良好的封装性
- 避免在 render 过程中读取或写入 ref.current
5. 最佳实践
- 使用 TypeScript 定义暴露的接口类型
- 合理划分暴露方法的粒度
- 文档化暴露的方法
- 遵循最小暴露原则
- 在清理阶段(cleanup)正确处理 ref,特别是涉及定时器等资源时
6. 使用场景建议
- 访问 DOM 元素或组件实例
- 存储定时器 ID
- 存储上一次的值
- 存储不需要触发重新渲染的数据
- 跨组件方法调用(通过 forwardRef)
通过合理使用 useRef,可以优化组件性能,实现更复杂的组件交互,同时保持代码的可维护性和可读性。