使用useRef
有段时间了,最近梳理了useRef
的使用细节。
一、动机
- 函数组件访问DOM元素;
- 函数组件访问之前渲染变量。
函数组件每次渲染都会被执行,函数内部的局部变量一般会重新创建,利用useRef
可以访问上次渲染的变量,类似类组件的实例变量
效果。
1.2 函数组件使用createRef
不行吗?
createRef
主要解决class
组件访问DOM元素问题,并且最佳实践是在组件周期内只创建一次(一般在构造函数里调用)。如果在函数组件内使用createRef
会造成每次render
都会调用createRef
:
function WithCreateRef() {
const [minus, setMinus] = useState(0);
// 每次render都会重新创建`ref`
const ref = React.createRef(null);
const handleClick = () => {
setMinus(minus + 1);
};
// 这里每次都是`null`
console.log(`ref.current=${ref.current}`)
useEffect(() => {
console.log(`denp[minus]>`, ref.current && ref.current.innerText);
}, [minus]);
return (
<div className="App">
<h1 ref={ref}>Num: {minus}</h1>
<button onClick={handleClick}>Add</button>
</div>
);
}
二、使用
2.1 基本语法
- 每次渲染
useRef
返回值都不变; ref.current
发生变化并不会造成re-render
;ref.current
发生变化应该作为Side Effect
(因为它会影响下次渲染),所以不应该在render
阶段更新current
属性。
2.2 不可以
在render
里更新ref.current
值
在Is there something like instance variables提到:
Unless you’re doing lazy initialization, avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects.
在render
里更新refs
导致什么问题呢?
在异步渲染里render
阶段可能会多次执行。
const RenderCounter = () => {
const counter = useRef(0);
// counter.current的值可能增加不止一次
counter.current = counter.current + 1;
return (
<h1>{`The component has been re-rendered ${counter.current} times`}</h1>
);
}
2.3 可以
在render
里更新ref.current
值
同样也是在Is there something like instance variables提到的:
Unless you’re doing lazy initialization, avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects.
为啥lazy initialization
却可以在render
里更新ref.current
值?
这个跟useRef
懒初始化的实现方案有关。
const instance = React.useRef(null)
if (instance.current == null) {
instance.current = {
// whatever you need
}
}
本质上只要保证每次render
不会造成意外效果,都可以在render阶段
更新ref.current
。但最好别这样,容易造成问题,useRef
懒初始化毕竟是个特殊的例外。
2.4 ref.current
不可以
作为其他hooks(useMemo
, useCallback
, useEffect
)依赖项
ref.current
的值发生变更并不会造成re-render
, Reactjs并不会跟踪ref.current
的变化。
function Minus() {
const [minus, setMinus] = useState(0);
const ref = useRef(null);
const handleClick = () => {
setMinus(minus + 1);
};
console.log(`ref.current=${ref.current && ref.current.innerText}`)
// #1 uesEffect
useEffect(() => {
console.log(`denp[ref.current] >`, ref.current && ref.current.innerText);
}, [ref.current]);
// #2 uesEffect
useEffect(() => {
console.log(`denp[minus]>`, ref.current && ref.current.innerText);
}, [minus]);
return (
<div className="App">
<h1 ref={ref}>Num: {minus}</h1>
<button onClick={handleClick}>Add</button>
</div>
);
}
本例子中当点击[Add]按钮两次后#1 uesEffect
就不会再执行了,如图:
原因分析:
依赖项判断是在render
阶段判断的,发生在在ref.current
更新之前,而useEffect
的effect函数执行在渲染之后。
第一次执行:
首次无脑执行,所以输出:ref.current=null denp[ref.current] > Num: 0 denp[minus]> Num: 0
并且此时
ref.current
为null
,所以#1 uesEffect
相当于useEffect(() => console.log('num 1'), [null])
点击[Add],第二次执行:
此时ref.current
值为<h1>Num: 0<h1>
,所以#1 uesEffect
的依赖项发生变化,最终输出:ref.current=Num: 0 denp[ref.current] > Num: 1 denp[minus]> Num: 1
此时
#1 uesEffect
相当于useEffect(() => console.log('num 1'), [<h1>Num: 0<h1>])
点击[Add],第三次执行:
此时ref.current
值为<h1>Num: 1<h1>
,所以#1 uesEffect
的依赖项没有发生变化,故#1 uesEffect
的effect函数不会被执行,最终输出:ref.current=Num: 1 denp[minus]> Num: 2
如果将ref.current
作为依赖项,eslint-plugin-react-hooks
也会报警提示的:
React Hook useEffect has an unnecessary dependency: 'ref.current'. Either exclude it or remove the dependency array. Mutable values like 'ref.current' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
2.5 ref
作为其他hooks(useMemo
, useCallback
, useEffect
)依赖项
ref
是不变的,没必要作为其他hooks依赖。
三、原理
本质上是记忆hook,但也可作为data hook,可以简单的用useState
模拟useRef
:
const useRef = (initialValue) => {
const [ref] = useState({ current: initialValue});
return ref
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。