useRef使用細節

使用useRef有段時間了,最近梳理了useRef的使用細節。html

1、動機

  1. 函數組件訪問DOM元素;
  2. 函數組件訪問以前渲染變量。

函數組件每次渲染都會被執行,函數內部的局部變量通常會從新建立,利用useRef能夠訪問上次渲染的變量,相似類組件的實例變量效果。react

1.2 函數組件使用createRef不行嗎?

createRef主要解決class組件訪問DOM元素問題,而且最佳實踐是在組件週期內只建立一次(通常在構造函數裏調用)。若是在函數組件內使用createRef會形成每次render都會調用createRefgit

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、使用

2.1 基本語法

見文檔github

  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提到:npm

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提到的:less

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懶初始化畢竟是個特殊的例外ide

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依賴。

3、原理

image
本質上是記憶hook,但也可做爲data hook,能夠簡單的用useState模擬useRef

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

參考

整理自gitHub筆記:useRef

相關文章
相關標籤/搜索