Understanding the use cases of useRef

從實例了解 useRef 的使用情境

useRef 說明

useRef 是一個 Hook 可以讓你參考一個值並不會受到 React 的重新渲染影響

以上是根據 React 官方文檔🔗useRef 的說明,但這句話這究竟代表什麼意思?簡單來說我會useRef 當作是 useState 但不會觸發重新渲染 ,React 會幫你記錄這筆資料,就算元件重新渲染了還是存在。

範例一:記錄元件的資料狀態,不受渲染影響

這麼說還是有些抽象,讓我們來模擬一個簡單的需求並嘗試實現它:「計算一個元件究竟被重新渲染了幾次」。首先在畫面上定義一個輸入框,當使用者輸入名字時,會在畫面上顯示「我的名字是 x 」以及「我重新渲染了 x 次」。

// 先定義模板的模樣
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<div>我的名字是 {name}</div>
<div>我重新渲染了 {renderCount} 次</div>
</div>

並且定義資料的部分,這裡使用 useState 來定義 namerenderCount 這兩個變數,並且在每次元件刷新時就讓 renderCount + 1。

// 處理資料的部分
const [name, setName] = useState('');
const [renderCount, setRenderCount] = useState(0);
useEffect(() => {
setRenderCount((prevCount) => prevCount + 1);
});

看起來很簡單,應該不會有什麼問題?但實際上前面的程式邏輯上有非常大的漏洞,它無法運作且會瘋狂跳出以下的錯誤!

Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

這是因為輸入導致觸發元件重新渲染,這時候觸發 renderCount + 1 又會導致元件重新渲染,進入無限的更新 State > 觸發渲染 > 更新 State > 觸發渲染 …… 😭,這時候就是使用 useRef 的典型時機。

只需要把 renderCount 改成使用 useRef 來定義,程式就可以如期運作了,這是因為 useRef 並不會造成元件重新渲染:

const [name, setName] = useState('');
const renderCount = useRef(1);
useEffect(() => {
renderCount.current += 1;
});

本元件重新渲染次數:1

在上面的例子中示範了如果需要在元件重新渲染時保留某些狀態,就可以使用 useRef 這個 Hook 來記錄。

範例二:選取元素

由於 React 是建構虛擬 DOM 來處理元件的渲染,所以我們無法直接使用 querySelector,這個方法只能取得真實的 DOM,而不是 React 的虛擬 DOM。在 React 中,我們可以使用 ref 來選取虛擬的 DOM 元素,使用 useRef 記錄下來。

const [name, setName] = useState('');
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);

在這個案例中,你可以想像就像是 Vitural DOM 版的 querySelector,只是這個方法可以在 React 的元件中使用。

總結

既然說到 useRef 就不能不談 useState 與其他定義變數的方式,它們有不同的用途和特點,讓我們來看看它們的差異:

方法一:定義變數在元件外

let outside = 0;
const App = () => {
return <div>{outside}</div>;
};

這麼做就相當於定義了一個全域變數,它會在加載整個應用程式的時候被建立,並且在整個應用程式中都可以使用,這種方式在某些情況下是很方便的,比如當我們有多個元件需要共享同一個變數時,但也會有一些潛在的問題像是:

  1. 命名衝突:如果多個元件都使用了同一個變數名稱,就會造成命名衝突。
  2. 難以追蹤:當變數被多個元件共享時,很難追蹤變數是如何被修改的。這可能會導致難以調試的問題。
  3. 難以測試:導致程式的複雜性和不穩定性上升且難以預估與測試。

綜合下來不會是一個很好的方法,如果希望狀態能夠在元件之間共享,可以傳遞 Props🔗 或使用 useContext🔗 來達成,而非使用全域變數。

方法二:定義變數在元件內

const App = () => {
let inside = 0;
return <div>{inside}</div>;
};

這麼做與方法一類似,只是變數被定義在元件內部,這樣就不會有命名衝突的問題,但一個很大的區別是,這個變數會在每次元件刷新時重置

方法三:使用 useState

相信學習 React 第一個碰的 Hook 就是 useState,因此也不贅述,就是當 setState 時,元件會重新渲染

const App = () => {
const [state, setState] = useState(0);
return <div>{state}</div>;
};

方法四:使用 useRef

useState 類似,但不會觸發重新渲染

const App = () => {
const ref = useRef(0);
return <div>{ref.current}</div>;
};

資料來源