useRef使用细节

使用useRef有段时间了,最近梳理了useRef的使用细节。

一、动机

  1. 函数组件访问DOM元素;
  2. 函数组件访问之前渲染变量。

函数组件每次渲染都会被执行,函数内部的局部变量一般会重新创建,利用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 基本语法

见文档

  1. 每次渲染useRef返回值都不变;
  2. ref.current发生变化并不会造成re-render;
  3. 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就不会再执行了,如图:
image

原因分析:
依赖项判断是在render阶段判断的,发生在在ref.current更新之前,而useEffect的effect函数执行在渲染之后。

  1. 第一次执行:
    首次无脑执行,所以输出:

    ref.current=null
    denp[ref.current] > Num: 0
    denp[minus]> Num: 0

    并且此时ref.currentnull,所以 #1 uesEffect相当于useEffect(() => console.log('num 1'), [null])

  2. 点击[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>])

  3. 点击[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依赖。

三、原理

image
本质上是记忆hook,但也可作为data hook,可以简单的用useState模拟useRef

const useRef = (initialValue) => {
  const [ref] = useState({ current: initialValue});
  return ref
}

参考

整理自gitHub笔记:useRef

Coder

2.6k 声望
52 粉丝
0 条评论
推荐阅读
解析position: sticky;
粘性定位position sticky元素采用正常的文档流布局(static),当其边框(border矩形)相对于最近的滚动祖先元素的内边框(即content矩形)的小于指定阈值时,则position sticky元素相对于该最近的滚动祖先元素固...

普拉斯强3阅读 2.4k评论 1

正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青57阅读 8.6k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy49阅读 7.3k评论 12

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs42阅读 7k评论 12

封面图
CSS 绘制一只思否猫
欢迎关注我的公众号:前端侦探练习 CSS 有一个比较有趣的方式,就是发挥想象,绘制各式各样的图案,比如来绘制一只思否猫?思否猫,SegmentFault 思否的吉祥物,是一只独一无二、特立独行、热爱自由的(&gt;^ω^&lt...

XboxYan48阅读 3.3k评论 14

封面图
「多图预警」完美实现一个@功能
一天产品大大向 boss 汇报完研发成果和产品业绩产出,若有所思的走出来,劲直向我走过来,嘴角微微上扬。产品大大:boss 对我们的研发成果挺满意的,balabala...(内心 OS:不听,讲重点)产品大大:咱们的客服 I...

wuwhs32阅读 3.5k评论 5

封面图
还在用 JS 做节流吗?CSS 也可以防止按钮重复点击
举个例子:一个保存按钮,为了避免重复提交或者服务器考虑,往往需要对点击行为做一定的限制,比如只允许每300ms提交一次,这时候我想大部分同学都会到网上直接拷贝一段throttle函数,或者直接引用lodash工具库

XboxYan35阅读 2.7k评论 2

封面图

Coder

2.6k 声望
52 粉丝
宣传栏