乐趣区

关于hooks: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 就不会再执行了,如图:

起因剖析:
依赖项判断是在 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 依赖。

三、原理


实质上是记忆 hook,但也可作为 data hook,能够简略的用 useState 模仿useRef

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

参考

整顿自 gitHub 笔记:useRef

退出移动版