精读React hooks(四):useRef的多维用途
在 React 里,我们经常听到 "everything is a component" 这样的说法。而为了保持组件的纯净性,React 强调声明式编程,减少直接操作 DOM 的情况。然而,有时我们仍然需要直接与 DOM 交互,或者访问某个组件的具体实例。在这些情况下,Refs 就派上用场了。
useRef 基础知识
// 定义
const inputRef = useRef(null);
// 使用
console.log(inputRef.current)
这是useRef
的使用示例,useRef
返回一个可变的 ref 对象,通过.current
可以获取保存在useRef
的值。看起来像是一个复杂版的useState
,那么useState
和useRef
有什么区别?为什么需要useRef
呢?
主要原因有两个:
- 持久性:
useRef
的返回对象在组件的整个生命周期中都是持久的,而不是每次渲染都重新创建。 - 不会触发渲染:当
useState
中的状态改变时,组件会重新渲染。而当useRef
的.current
属性改变时,组件不会重新渲染。
总结来说,useRef
既能保存状态,还不会在更新时触发渲染。本文我们就来盘点一下useRef
的使用场景。
useRef 的常见用途
访问 DOM 元素
当我们需要直接与 DOM 元素进行交互(例如,手动获取焦点或测量元素尺寸)时,可以使用 useRef
。
function TextInput() {
const inputRef = useRef(null);
function focusInput() {
inputRef.current.focus();
}
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus the input</button>
</div>
);
}
我们还可以在组件嵌套的场景使用useRef
import { forwardRef, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
保存状态但不触发渲染
有时,你可能需要在组件中保存某些值,而不希望每次该值更改时都重新渲染组件。在这种情况下,useRef
很有用。
function Timer() {
const count = useRef(0);
useEffect(() => {
const intervalId = setInterval(() => {
count.current += 1;
console.log(`Elapsed time: ${count.current} seconds`);
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <div>Check the console to see the elapsed time!</div>;
}
这个示例完美说明了可以把useRef
视为一个能够在组件的整个生命周期中持久保存数据的“盒子”,而不会引起组件的重新渲染。
保存上一次的 props 或 state
在某些情况下,你可能需要知道 props 或 state 的上一次值。这时可以使用useRef
结合useEffect
来达到目的。
function DisplayValue({ value }) {
const [prevValue, setPrevValue] = useState(null); // 初始时,没有前一个值
const previousValue = useRef(value);
useEffect(() => {
setPrevValue(currentRef.current);
previousValue.current = value;
}, [value]);
return (
<div>
Current Value: {value} <br />
Previous Value: {prevValue}
</div>
);
}
当组件首次渲染时,previousValue.current
会被初始化为value
的当前值。随后,每当value
发生变化时,useEffect
都会运行并更新previousValue.current
为新的value
。
但这里有一个微妙之处:由于useEffect
是在组件渲染之后运行的,因此在组件的渲染过程中,previousValue.current
的值是从前一次渲染中保持不变的。只有当useEffect
被调用并执行完毕后,previousValue.current
才会更新为新的value
。
高级技巧
避免在渲染期间读/写 ref
function DisplayValue({ value }) {
const previousValue = useRef(value);
// 错误:在渲染期间修改 ref
if (previousValue.current !== value) {
previousValue.current = value;
}
return (
<div>
Current Value: {value} <br />
{/* 错误:在渲染期间读 ref */}
Previous Value: {previousValue.current}
</div>
);
}
这里,我们尝试在组件的渲染期间更新previousValue.current
。这违反了 React 的工作方式,并可能导致不可预测的行为。例如:
- 不稳定的 UI:由于 React 在多次渲染中可能使用异步和优化技术,直接在渲染期间修改 refs 可能导致 UI 不一致。
- 依赖更新:如果其他效应或钩子依赖于 ref 的值,它们可能不会在期望的时刻运行,因为直接修改 ref 不会触发重新渲染或其他效应。
这是为什么我们通常在useEffect
内部更新 refs。在useEffect
内部,我们可以确保组件已经完成渲染,并且不会在渲染期间发生任何不期望的副作用。
避免重复创建 ref
如果我们在创建 ref 时,想要通过计算或有副作用的方法获取初值,可能会用下面这种写法。这种写法会导致getInitialCount()
在每次组建渲染的时候都被调用。虽然useRef
的设计让它只从首次渲染的时候获取初值,但这种做法仍然会造成不必要的性能损耗。
function ClickCounter() {
// bad。这里的问题是,每次组件渲染时,getInitialCount都会被调用,尽管它的返回值只在第一次渲染时被使用。
const countRef = useRef(getInitialCount());
function handleClick() {
countRef.current += 1;
console.log(`Button clicked ${countRef.current} times.`);
}
return <button onClick={handleClick}>Click me!</button>;
}
解决这种场景下的 ref 创建也很简单,那就是用null
作为初始值,渲染的过程判断仅在null时去计算或调用有副作用的方法。
function ClickCounter() {
// good
const countRef = useRef(null);
// good
if (countRef.current === null) {
countRef.current = getInitialCount();
}
function handleClick() {
countRef.current += 1;
console.log(`Button clicked ${countRef.current} times.`);
}
return <button onClick={handleClick}>Click me!</button>;
}
与 useReducer 使用
当我们需要复杂的状态逻辑且希望避免额外的渲染时,可以考虑将useRef
与useReducer
结合使用。
例如:跟踪useReducer
的 action 数量。
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const actionsCountRef = useRef(0);
function handleIncrement() {
dispatch({ type: "increment" });
actionsCountRef.current += 1;
console.log(`Actions count: ${actionsCountRef.current}`);
}
return (
<>
Count: {state.count}
<button onClick={handleIncrement}>Increment</button>
</>
);
}
与第三方库集成
在使用非 React 库(如 D3、jQuery)时,我们可能需要使用useRef
来获得对真实 DOM 节点的引用。
例如:结合D3
import { useRef, useEffect } from 'react';
import * as d3 from 'd3';
function BarChart() {
const chartRef = useRef(null);
useEffect(() => {
const svg = d3.select(chartRef.current);
// ... 使用 D3 进行图表绘制
}, []);
return <svg ref={chartRef}></svg>;
}
动画处理
通过useRef
获取元素并使用 Web API 如requestAnimationFrame
可以实现复杂的动画效果。
import { useEffect, useRef } from "react";
function MovingBox() {
const boxRef = useRef(null);
const animationFrameRef = useRef(null);
useEffect(() => {
const boxElem = boxRef.current;
let position = 0;
const animate = () => {
position += 1;
if (position > window.innerWidth) {
position = -100; // 如果方块移动到屏幕的右侧,则从左侧重新开始
}
boxElem.style.transform = `translateX(${position}px)`;
animationFrameRef.current = requestAnimationFrame(animate);
};
animationFrameRef.current = requestAnimationFrame(animate);
return () => {
cancelAnimationFrame(animationFrameRef.current); // 在组件卸载时取消动画
};
}, []);
return (
<div
ref={boxRef}
style={{ width: "100px", height: "100px", background: "blue" }}
></div>
);
}
export default MovingBox;
事件监听
使用useRef
监听不由 React 管理的 DOM 事件。
例如:窗口大小变化
function WindowSize() {
const widthRef = useRef(window.innerWidth);
useEffect(() => {
const handleResize = () => {
widthRef.current = window.innerWidth;
console.log(`Width: ${widthRef.current}`);
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return <div>Check the console for window width updates!</div>;
}
结语
在本篇文章中,我们从基本的 DOM 引用出发,探讨了各种实际的应用场景,包括性能优化和动画方面。通过深入了解并有效使用 useRef
,我们可以更灵活地管理组件内部的状态,而不必担心触发不必要的渲染。希望这篇文章能帮助你更好地理解useRef
并能让你有所启发。
以上多个重要示例的实际效果都可以在我的示例站查看,TypeScript版的源码也已发布到我的Github:useRef分支。
专栏资源
专栏博客地址:精读React Hooks
专栏演示站:React Hooks Demos
专栏源码仓库:👉Github - Source Code
交个朋友:👉加入「独立全栈交流群」
专栏文章列表:
精读React hooks(一):useState 的几个基础用法和进阶技巧
精读React hooks(二):React状态管理的强大工具——useReducer
精读React hooks(三):useContext从基础应用到性能优化
精读React hooks(五):useEffect使用细节知多少?
精读React hooks(六):useLayoutEffect解决了什么问题?
精读React hooks(七):用useMemo来减少性能开销
精读React hooks(八):我们为什么需要useCallback
精读React hooks(九):使用useTransition进行非阻塞渲染
精读React hooks(十):使用useDeferredValue延迟状态更新
精读React hooks(十一):useInsertionEffect——CSS-in-JS样式注入新方式
精读React hooks(十二):使用useImperativeHandle能获得什么能力
精读React hooks(十三):使用useSyncExternalStore获取实时数据
精读React hooks(十四):总有一天你会需要useId为你生成唯一id