useRef使用细节

共 3845字,需浏览 8分钟

 ·

2020-09-23 22:16

作者:普拉斯强   

来源:SegmentFault 思否社区 





一、动机


  • 函数组件访问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 (
    "App">
      Num: {minus}
      Add
    

  );
}




二、使用


2.1 基本语法


见链接:https://reactjs.org/docs/hooks-reference.html#useref


  • 每次渲染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 (
    

{`The component has been re-rendered ${counter.current} times`}


  );
}


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.4ref.current不可以作为其他hooksuseMemo, 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 (
    "App">
      Num: {minus}
      Add
    

  );
}


本例子中当点击[Add]按钮两次后#1 uesEffect就不会再执行了,如图:



原因分析:


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


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



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

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




2、点击[Add],第二次执行:
此时ref.current值为

Num: 0

,所以 #1 uesEffect的依赖项发生变化,最终输出:


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

此时 #1 uesEffect相当于useEffect(() => console.log('num 1'), [

Num: 0

])


3、点击[Add],第三次执行:

此时ref.current值为

Num: 1

,所以 #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
}



点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流。


- END -

浏览 8
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报