大家好,我是小杨。今天想和大家聊聊React中一个既强大又容易让人困惑的特性------Ref。就像它的名字一样,Ref确实是React世界中的一条"秘密通道",让我们能够直接与DOM元素对话。
初识Ref:为什么需要"走后门"?
还记得我刚学React时,被告知要"忘记DOM操作",一切通过状态驱动。但很快我就遇到了这样的需求:
javascript
function SearchBox() {
const [query, setQuery] = useState('');
const handleFocus = () => {
// 我想让输入框自动获得焦点,但怎么直接操作DOM呢?
// document.getElementById('search-input').focus() ?
// 这违背了React的原则!
};
return (
<input
id="search-input"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
);
}
这时候,Ref就像一把钥匙,打开了直接操作DOM的大门。
Ref的三种武功秘籍
1. useRef Hook - 现代React的首选
javascript
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// 组件挂载后自动聚焦
inputRef.current.focus();
}, []);
return (
<input
ref={inputRef}
placeholder="我会自动获得焦点!"
/>
);
}
2. createRef - 类组件中的老将
javascript
class TraditionalInput extends Component {
constructor(props) {
super(props);
this.inputRef = createRef();
}
componentDidMount() {
this.inputRef.current.focus();
}
render() {
return <input ref={this.inputRef} />;
}
}
3. 回调Ref - 更灵活的选择
javascript
function CallbackRefExample() {
const [inputElement, setInputElement] = useState(null);
useEffect(() => {
if (inputElement) {
inputElement.focus();
}
}, [inputElement]);
return (
<input ref={setInputElement} />
);
}
Ref的实际战斗场景
场景1:管理焦点、文本选择
javascript
function LoginForm() {
const emailRef = useRef(null);
const passwordRef = useRef(null);
const handleEmailSubmit = () => {
// 邮箱输入完成后,自动跳到密码框
passwordRef.current.focus();
};
return (
<form>
<input
ref={emailRef}
placeholder="邮箱"
onBlur={handleEmailSubmit}
/>
<input
ref={passwordRef}
placeholder="密码"
type="password"
/>
</form>
);
}
场景2:集成第三方DOM库
javascript
function ChartComponent() {
const chartContainerRef = useRef(null);
useEffect(() => {
// 使用D3.js等第三方库时,需要直接操作DOM
const chart = d3.select(chartContainerRef.current)
.append('svg')
// ... 更多D3操作
return () => {
// 清理工作
chart.remove();
};
}, []);
return <div ref={chartContainerRef} />;
}
场景3:获取组件尺寸
javascript
function ResponsiveComponent() {
const containerRef = useRef(null);
const [dimensions, setDimensions] = useState({});
useEffect(() => {
const updateSize = () => {
if (containerRef.current) {
setDimensions({
width: containerRef.current.offsetWidth,
height: containerRef.current.offsetHeight
});
}
};
updateSize();
window.addEventListener('resize', updateSize);
return () => window.removeEventListener('resize', updateSize);
}, []);
return (
<div ref={containerRef}>
当前尺寸: {dimensions.width} x {dimensions.height}
</div>
);
}
场景4:实现动画效果
javascript
function FadeInBox() {
const boxRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
if (boxRef.current) {
observer.observe(boxRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div
ref={boxRef}
style={{
opacity: 0,
transform: 'translateY(20px)',
transition: 'all 0.5s ease'
}}
>
我会在进入视口时淡入!
</div>
);
}
进阶技巧:forwardRef - 让Ref穿透组件
有时候,我们需要在自定义组件上使用Ref:
javascript
// 错误示范:这样Ref不会传递到input上
function MyInput(props) {
return <input {...props} />;
}
// 正确做法:使用forwardRef
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
// 使用方式
function ParentComponent() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <MyInput ref={inputRef} placeholder="我可以获得焦点" />;
}
useImperativeHandle:控制Ref的暴露内容
javascript
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 只暴露focus方法,隐藏其他DOM API
focus: () => {
inputRef.current.focus();
},
// 自定义方法
shake: () => {
// 实现抖动动画
}
}));
return <input ref={inputRef} {...props} />;
});
// 使用时,只能调用暴露的方法
function App() {
const fancyInputRef = useRef(null);
const handleClick = () => {
fancyInputRef.current.focus(); // 可以
// fancyInputRef.current.value = 'hello'; // 错误!没有暴露value
};
return (
<>
<FancyInput ref={fancyInputRef} />
<button onClick={handleClick}>聚焦</button>
</>
);
}
我踩过的坑:Ref使用注意事项
1. Ref不是状态!
javascript
function BadExample() {
const countRef = useRef(0);
const increment = () => {
countRef.current += 1;
// 不会触发重新渲染!
console.log(countRef.current); // 值变了,但UI不会更新
};
return (
<div>
<button onClick={increment}>增加</button>
{/* 这里永远显示0 */}
<span>计数: {countRef.current}</span>
</div>
);
}
2. 不要在渲染期间操作Ref
javascript
function DangerousComponent() {
const ref = useRef(null);
// 错误!不要在渲染期间操作DOM
if (ref.current) {
ref.current.style.color = 'red';
}
return <div ref={ref}>Hello</div>;
}
我的实战经验总结
该用Ref的时候:
- 管理焦点、文本选择、媒体播放
- 触发强制动画
- 集成第三方DOM库
- 需要直接测量DOM元素
不该滥用Ref的时候:
- 能用状态驱动解决的问题
- 组件间的数据传递(应该用props)
- 派生数据计算
记住Ref是React的"逃生舱",在大多数情况下,我们应该优先考虑React的数据流和状态管理。只有在真正需要直接操作DOM时,才请出这个强大的工具。
⭐ 写在最后
请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.
✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式
✅ 认为我部分代码过于老旧,可以提供新的API或最新语法
✅ 对于文章中部分内容不理解
✅ 解答我文章中一些疑问
✅ 认为某些交互,功能需要优化,发现BUG
✅ 想要添加新功能,对于整体的设计,外观有更好的建议
✅ 一起探讨技术加qq交流群:906392632
最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!