React ref & useRef 完全指南

在這篇文章中,你將學習如何使用React.useRef()鉤子來建立持久的可變值(也稱為references或refs),以及訪問DOM元素。

我們將從下面幾點講解:

1
2
3
4
5
6
7
8
9
1. 可變值
- 1.1用例:紀錄按鈕點選
- 1.2用例:實現秒錶

2. 訪問DOM元素
- 2.1用例:聚焦輸入

3.更新引用限制
4. 總結

可變值

useRef(initialValue)接受一個引數(引用的初始值)並回傳一個引用(也稱為ref)。引用只是一個具有特殊屬性current的物件:

1
2
3
const reference = useRef(initialValue);

reference.current; // 當前的引用

reference.current 訪問引用,並且 reference.current = newValue 更新引用值:

1
2
3
4
5
6
7
8
9
10
11
12
import {<!-- --> useRef } from 'react';

function MyComponent() {<!-- -->
  const reference = useRef(initialValue);

  const someHandler = () => {<!-- -->
    // 訪問引用
    const value = reference.current;
    // 更新引用值
    reference.current = newValue;
  };
}

關於 references 有兩點需要記住:

  1. 在套件重新渲染之間,引用的值是永續化的(保持不變);
  2. 更新引用不會觸發套件重新呈現。

現在,讓我們看看如何在實踐中使用 useRef()

例項:紀錄按鈕點選

套件logbuttonclicked使用了一個引用來儲存按鈕的點選次數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {<!-- --> useRef } from 'react';

function LogButtonClicks() {<!-- -->
  const countRef = useRef(0);
 
  const handle = () => {<!-- -->
    countRef.current++;
    console.log(`Clicked ${<!-- -->countRef.current} times`);
  };

  console.log('I rendered!');

  return <button onClick={<!-- -->handle}>Click me</button>;
}

const countRef = useRef(0)建立一個用0初始化的引用countRef

當按鈕被單擊時,handle函式被呼叫,並且引用值被遞增:countRef.current++,該引用值被紀錄到主控臺。

注意,更新引用值countRef.current++不會觸發套件重新渲染。
'I rendered!'在初始渲染時只會輸出一次。

現在有一個合理的問題:引用和狀態之間的主要區別是什麼?

現在有一個合理的問題:referencesstate之間的主要區別是什麼?

reference 和 state 之間的主要區別

讓我們重用上一節中的logbuttonclicked套件,但使用useState()鉤子來計算按鈕的點選次數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {<!-- --> useState } from 'react';

function LogButtonClicks() {<!-- -->
  const [count, setCount] = useState(0);
 
  const handle = () => {<!-- -->
    const updatedCount = count + 1;
    console.log(`Clicked ${<!-- -->updatedCount} times`);
    setCount(updatedCount);
  };

  console.log('I rendered!');

  return <button onClick={<!-- -->handle}>Click me</button>;
}

每次點選,你會在主控臺中看到「I rendering !」』——這意味著每次狀態更新時,套件都會重新呈現

所以,state和references之間的兩個主要區別是:

  1. 更新 state 會觸發套件重新呈現,而更新 ref 則不會。
  2. state 更新是非同步的(state變數在重新呈現後更新),而ref則同步更新(更新後的值立即可用)

從更高的角度來看,ref 用於儲存套件的基礎設施資料,而 state 儲存直接呈現在螢幕上的訊息

例項:實現秒錶

你可以儲存在 ref 中的東西是涉及到一些副作用的基礎設施訊息。例如,你可以在ref中儲存不同型態的指標:定時器id,套接字id,等等。

例如,下面的秒錶套件使用setInterval(回呼,時間)計時器函式來增加秒錶計數器的每一秒。定時器id儲存在一個引用timerIdRef:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {<!-- --> useRef, useState, useEffect } from 'react';

function Stopwatch() {<!-- -->
  const timerIdRef = useRef(0);
  const [count, setCount] = useState(0);

  const startHandler = () => {<!-- -->
    if (timerIdRef.current) {<!-- --> return; }
    timerIdRef.current = setInterval(() => setCount(c => c+1), 1000);
  };

  const stopHandler = () => {<!-- -->
    clearInterval(timerIdRef.current);
    timerIdRef.current = 0;
  };

  useEffect(() => {<!-- -->
    return () => clearInterval(timerIdRef.current);
  }, []);

  return (
    <div>
      <div>Timer: {<!-- -->count}s</div>
      <div>
        <button onClick={<!-- -->startHandler}>Start</button>
        <button onClick={<!-- -->stopHandler}>Stop</button>
      </div>
    </div>
  );
}

startHandler()函式在單擊Start按鈕時呼叫,它啟動計時器並在引用timerIdRef.current= setInterval(…)中儲存計時器id。

要停止秒錶,請單擊「停止」按鈕。停止按鈕處理程式stopHandler()從引用中訪問計時器id並停止計時器clearInterval(timerIdRef.current)

此外,如果套件在秒錶處於活動狀態時解除安裝,useEffect()的清理函式也將停止計時器。

在秒錶範例中,ref用於儲存基礎架構資料—活動計時器id。

訪問 DOM 元素

useRef()鉤子的另一個有用的應用是訪問DOM元素。這需要三個步驟:

  1. 定義訪問元素const elementRef = useRef()的引用;

  2. 將引用賦給元素的ref屬性:

    ;

  3. 引用完成後,elementRef.current包含DOM元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {<!-- --> useRef, useEffect } from 'react';

function AccessingElement() {<!-- -->
  const elementRef = useRef();

   useEffect(() => {<!-- -->
    const divElement = elementRef.current;
  }, []);

  return (
    <div ref={<!-- -->elementRef}>
      I'm an element
    </div>
  );
}

例項:聚焦輸入框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {<!-- --> useRef, useEffect } from 'react';

function InputFocus() {<!-- -->
  const inputRef = useRef();

  useEffect(() => {<!-- -->
    inputRef.current.focus();
  }, []);

  return (
    <input
      ref={<!-- -->inputRef}
      type="text"
    />
  );
}

const inputRef = useRef()建立一個引用來儲存輸入元素。

然後將inputRef賦值給輸入欄位的ref屬性:

然後,設定inputRef 作為輸入元素。現在您可以透過程式設計方式將焦點設定為輸入狀態:inputRef.current.focus()

在初始化渲染時 Ref 是 null

在初始渲染時,儲存DOM元素的 ref 是空的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {<!-- --> useRef, useEffect } from 'react';

function InputFocus() {<!-- -->
  const inputRef = useRef();

  useEffect(() => {<!-- -->
    // 輸出 `HTMLInputElement`
    console.log(inputRef.current);

    inputRef.current.focus();
  }, []);

  // 初始化渲染時輸出 `undefined`
  console.log(inputRef.current);

  return <input ref={<!-- -->inputRef} type="text" />;
}

在初始渲染期間,React仍然決定套件的輸出,因此還沒有建立DOM結構。這就是為什麼inputRef。current在初始呈現時計算為undefined

當輸入元素在DOM中建立完成後,useEffect(callback,[])鉤子立即呼叫回呼函式:因此回呼函式是訪問inputRef.current的正確位置。

更新 references 限制

功能套件的功能範圍應該計算輸出或呼叫鉤子。

這就是為什麼更新 ref (以及更新 state)不應該在套件函式的直接作用域內執行。

ref必須在useEffect()回呼或處理程式(事件處理程式、計時器處理程式等)內部更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {<!-- --> useRef, useEffect } from 'react';

function MyComponent({<!-- --> prop }) {<!-- -->
  const myRef = useRef(0);

  useEffect(() => {<!-- -->
    myRef.current++; // Good!

    setTimeout(() => {<!-- -->
      myRef.current++; // Good!
    }, 1000);
  }, []);

  const handler = () => {<!-- -->
    myRef.current++; // Good!
  };

  myRef.current++; // Bad!

  if (prop) {<!-- -->
    myRef.current++; // Bad!
  }

  return <button onClick={<!-- -->handler}>My button</button>;
}

總結

useRef()鉤子儲存可變的值(又名references或refs),這些值在渲染之間永續化,也可以訪問DOM元素。

使用初始值呼叫const reference = useRef(initialValue)會回傳一個名為reference的特殊物件。引用物件有一個屬性current:可以使用該屬性讀取引用值,或更新引用。reference.current = newValue

在套件重新呈現之間,引用的值是持久的。

更新引用與更新狀態相反,不會觸發套件重新呈現。

引用也可以訪問DOM元素。

element

- 元素在reference.current中是可用的。