前言
React Ref(引用)是React中一个强大而重要的概念,它为我们提供了直接访问DOM元素或组件实例的能力。虽然React推崇声明式编程和数据驱动的理念,但在某些场景下,我们仍需要直接操作DOM或访问组件实例。本文将深入探讨React Ref的工作原理、使用方法和最佳实践。
什么是React Ref?
React Ref是一个可以让我们访问DOM节点或在render方法中创建的React元素的方式。它本质上是一个对象,包含一个current
属性,用于存储对真实DOM节点或组件实例的引用。
为什么需要Ref?
在React的声明式编程模型中,数据流是单向的:props向下传递,事件向上冒泡。但在以下场景中,我们需要直接访问DOM或组件:
- 管理焦点、文本选择或媒体播放
- 触发强制动画
- 集成第三方DOM库
- 测量DOM元素的尺寸
- 访问子组件的方法
Ref的演进历史
1. String Refs(已废弃)
javascript
// 不推荐使用
class MyComponent extends React.Component {
componentDidMount() {
this.refs.myInput.focus();
}
render() {
return <input ref="myInput" />;
}
}
String Refs存在性能问题和潜在的内存泄漏风险,已在React 16.3中被废弃。
2. Callback Refs
javascript
class MyComponent extends React.Component {
setInputRef = (element) => {
this.inputElement = element;
}
componentDidMount() {
if (this.inputElement) {
this.inputElement.focus();
}
}
render() {
return <input ref={this.setInputRef} />;
}
}
3. createRef(React 16.3+)
javascript
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
componentDidMount() {
this.inputRef.current.focus();
}
render() {
return <input ref={this.inputRef} />;
}
}
4. useRef Hook(React 16.8+)
javascript
function MyComponent() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
深入理解useRef
useRef的基本用法
useRef
返回一个可变的ref对象,其.current
属性被初始化为传入的参数。
javascript
const refContainer = useRef(initialValue);
useRef的特点
- 持久化存储:useRef在组件的整个生命周期中保持同一个引用
- 不触发重新渲染 :修改
.current
属性不会触发组件重新渲染 - 同步更新 :
.current
的值会同步更新,不像state那样异步
useRef vs useState
javascript
function RefVsState() {
const [stateValue, setStateValue] = useState(0);
const refValue = useRef(0);
const updateState = () => {
setStateValue(prev => prev + 1);
console.log('State value:', stateValue); // 异步更新,可能显示旧值
};
const updateRef = () => {
refValue.current += 1;
console.log('Ref value:', refValue.current); // 同步更新,显示新值
};
return (
<div>
<p>State: {stateValue}</p>
<p>Ref: {refValue.current}</p>
<button onClick={updateState}>Update State</button>
<button onClick={updateRef}>Update Ref</button>
</div>
);
}
Ref的实际应用场景
1. 访问DOM元素
javascript
function FocusInput() {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current.focus();
};
const handleClear = () => {
inputRef.current.value = '';
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleFocus}>Focus Input</button>
<button onClick={handleClear}>Clear Input</button>
</div>
);
}
2. 存储可变值
javascript
function Timer() {
const [time, setTime] = useState(0);
const intervalRef = useRef(null);
const start = () => {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
setTime(prev => prev + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
return (
<div>
<p>Time: {time}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
3. 保存上一次的值
javascript
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function MyComponent({ count }) {
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
</div>
);
}
高级Ref技巧
1. forwardRef
forwardRef
允许组件将ref转发到其子组件:
javascript
const FancyInput = React.forwardRef((props, ref) => (
<input ref={ref} className="fancy-input" {...props} />
));
function Parent() {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current.focus();
};
return (
<div>
<FancyInput ref={inputRef} />
<button onClick={handleFocus}>Focus Input</button>
</div>
);
}
2. useImperativeHandle
useImperativeHandle
可以自定义暴露给父组件的实例值:
javascript
const CustomInput = React.forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
scrollIntoView: () => {
inputRef.current.scrollIntoView();
},
getValue: () => {
return inputRef.current.value;
}
}));
return <input ref={inputRef} {...props} />;
});
function Parent() {
const customInputRef = useRef(null);
const handleAction = () => {
customInputRef.current.focus();
console.log(customInputRef.current.getValue());
};
return (
<div>
<CustomInput ref={customInputRef} />
<button onClick={handleAction}>Focus and Get Value</button>
</div>
);
}
3. Ref回调函数
javascript
function MeasureElement() {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const measureRef = useCallback((node) => {
if (node !== null) {
setDimensions({
width: node.getBoundingClientRect().width,
height: node.getBoundingClientRect().height
});
}
}, []);
return (
<div>
<div ref={measureRef} style={{ padding: '20px', border: '1px solid #ccc' }}>
Measure me!
</div>
<p>Width: {dimensions.width}px</p>
<p>Height: {dimensions.height}px</p>
</div>
);
}
最佳实践与注意事项
1. 避免过度使用Ref
javascript
// ❌ 不推荐:过度使用ref
function BadExample() {
const inputRef = useRef(null);
const [value, setValue] = useState('');
const handleChange = () => {
setValue(inputRef.current.value); // 不必要的ref使用
};
return <input ref={inputRef} onChange={handleChange} />;
}
// ✅ 推荐:使用受控组件
function GoodExample() {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
};
return <input value={value} onChange={handleChange} />;
}
2. 检查ref的有效性
javascript
function SafeRefUsage() {
const elementRef = useRef(null);
const handleClick = () => {
// 总是检查ref是否有效
if (elementRef.current) {
elementRef.current.focus();
}
};
return (
<div>
<input ref={elementRef} />
<button onClick={handleClick}>Focus</button>
</div>
);
}
3. 清理副作用
javascript
function ComponentWithCleanup() {
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
console.log('Interval running');
}, 1000);
// 清理函数
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
return <div>Component with cleanup</div>;
}
4. 避免在渲染期间访问ref
javascript
// ❌ 不推荐:在渲染期间访问ref
function BadRefUsage() {
const inputRef = useRef(null);
// 渲染期间访问ref可能为null
const inputValue = inputRef.current?.value || '';
return <input ref={inputRef} placeholder={inputValue} />;
}
// ✅ 推荐:在effect或事件处理器中访问ref
function GoodRefUsage() {
const inputRef = useRef(null);
const [placeholder, setPlaceholder] = useState('');
useEffect(() => {
if (inputRef.current) {
setPlaceholder(inputRef.current.value || 'Enter text');
}
});
return <input ref={inputRef} placeholder={placeholder} />;
}
性能考虑
1. 使用useCallback优化ref回调
javascript
function OptimizedRefCallback() {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
// 使用useCallback避免不必要的重新渲染
const measureRef = useCallback((node) => {
if (node !== null) {
const rect = node.getBoundingClientRect();
setDimensions({ width: rect.width, height: rect.height });
}
}, []);
return <div ref={measureRef}>Measured content</div>;
}
2. 避免内联ref回调
javascript
// ❌ 不推荐:内联ref回调
function InlineRefCallback() {
const [element, setElement] = useState(null);
return (
<div ref={(node) => setElement(node)}>
Content
</div>
);
}
// ✅ 推荐:使用useCallback
function OptimizedRefCallback() {
const [element, setElement] = useState(null);
const refCallback = useCallback((node) => {
setElement(node);
}, []);
return <div ref={refCallback}>Content</div>;
}
实际项目示例
自定义Hook:useClickOutside
javascript
function useClickOutside(callback) {
const ref = useRef(null);
useEffect(() => {
const handleClick = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
callback();
}
};
document.addEventListener('mousedown', handleClick);
return () => {
document.removeEventListener('mousedown', handleClick);
};
}, [callback]);
return ref;
}
// 使用示例
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useClickOutside(() => setIsOpen(false));
return (
<div ref={dropdownRef}>
<button onClick={() => setIsOpen(!isOpen)}>
Toggle Dropdown
</button>
{isOpen && (
<div className="dropdown-menu">
<p>Dropdown content</p>
</div>
)}
</div>
);
}