应用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=nulldenp[ref.current] > Num: 0denp[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: 0denp[ref.current] > Num: 1denp[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: 1denp[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