接下來幾篇文章將會針對 React 常見的 Hooks 的使用目的與場景 ,內容主要都是透過官方文件、一些大師影片以及自身開發經驗彙整出來的,如果有很不錯的學習資源我也會在文末留下來源供大家參考喔!

首篇就來一個最近開發中很常使用到的 useRef

useRef

認識一下吧 useRef

先讓我們來看看 React 官方文件的 useRef 定義:

useRef is a React Hook that lets you reference a value that’s not needed for rendering.

簡單來說有與 state 相似的功能都是暫存值,但最大的不同就是 useRef 裡的 ref object 就算變動了也不會造成畫面的 re-render,是完全隔絕於 component life cycle 外的狀態。

useRef 實用場景

1. Referencing a value with a ref(使用 ref 對照值)

這是 useRef 最基本的用法。可以用它來創建一個「可變」的 ref object,object 的 .current 屬性被初始化為傳遞給 useRef() 的參數,在後續的 re-render 中,useRef 將返回相同的 object。而且這個 object 可以更改其 current 屬性以存儲為新的值,就算值更新了也不會讓整個畫面 re-render。

接著舉兩個常見的使用案例:

🔎 ex1. 存取最新的值

若我們想要在若干秒數後取得某一個狀態的最新值,useRef 就可以派上用場了!!

1
2
3
4
5
6
7
8
9
const [timeoutCount, setTimeoutCount] = useState(0);
const countRef = useRef(count);
countRef.current = count;

const getCountTimeout = () => {
setTimeout(() => {
setTimeoutCount(countRef.current);
}, 5000);
};

這邊為什麼使用 ref.current 存值,而不是用 state

因為當 setTimeout 被建立時,它使用的是當時 count 的值,它依賴一個閉包(closure)來異步訪問 count。因此當組件重新渲染時,一個新的閉包會被創建,但這不會改變最初被封閉的值,所以如果用了 state,當 5 秒後你以為 count 已經變更了拿出來用,會發現該 state 還是初始的 0。

這就是 useRef 的一個實用情景可以避免上述的情形發生,我們透過將狀態值與 ref 的 current 屬性同步,並在 timeout 中讀取 current以取得我們想要的值。

🔎 ex2. 存取「前一個值」

呈上,我們當然也可以拿來存取前一個值。

1
2
3
4
5
6
7
8
9
10
const [name, setName] = useState('');
const prevName = useRef('');

useEffect(() => {
prevName.current = name;
},[name])

return (
<div>My name is {name}, and it used to be {prevName.current}.</div>
)

2. Manipulating the DOM with a ref(使用 ref 操作 DOM )

在 React 中我們可以使用 useRef 來直接操作 DOM 元素。通過將 ref 對象的 .current 屬性設置為一個 DOM 節點,可以直接讀取或修改這個節點。

🔎 ex1. 取用某一個 DOM 作進一步操作

最簡單常用的使用情境包含列印、操作特定的 DOM Scroll to view、監聽無限滾動的最後一個元素等等,這邊舉列印為例:

1
2
3
4
5
6
7
8
9
10
11
const printRef = useRef<HTMLDivElement>(null);
const handlePrint = useReactToPrint({
content: () => printRef.current
});

return (
<PrintArea ref={ref}>
// 列印內容略
</PrintArea>
)

🔎 ex2. 父組件使用 useRef 調用子組件的方法

在 react 的父子層的關係,父層若要呼叫存在子層的函式通常會將函式放置在父層,並且將函式與相關的狀態一起傳入子層中做使用。也是之前分享過的文章【圖解】React 常見的父子傳值方法 Props(https://leewanhsuan.github.io/2022/04/25/01-props/)所提過的方法。

而下方改成以 useRef 的方式實踐,這段範例中使用 React.forwardRefReact.useImperativeHandle 來從父組件調用子組件的方法,這樣可以把方法保留在子層中,卻又能在父層中調用。

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
31
32
33
34
35
import React, { useRef, useEffect } from 'react';

const ChildComponent = React.forwardRef((props, ref) => {
// 這個方法會被父組件調用
const method = () => {
console.log('Child component method called!');
};

// 使用 ref 將方法提供給父組件
React.useImperativeHandle(ref, () => ({
someMethod,
}));

return <div>Child Component</div>;
});

const ParentComponent = () => {
const childRef = useRef(null);

useEffect(() => {
// 使用 ref 調用子組件的 method 方法
if (childRef.current) {
childRef.current.method();
}
}, []);

return (
<div>
<h1>Parent Component</h1>
<ChildComponent ref={childRef} />
</div>
);
};

export default ParentComponent;

3. Avoiding recreating the ref contents(避免不必要的重建 ref 內容)

使用 useRef 可以幫助你避免在重新渲染 component 時不必要地重建 ref 的內容,因為 ref 會在 re-render 中保持不變,例如一些資源很大的影音 component 就可以使用此方法,避免重複拿取資源、更新畫面。

1
2
3
4
5
6
7
const Video = () => {
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
// ...
}

以上就是 useRef 官方提供的三種使用情境以及對應的範例,在整理的過程中也讓我更加認識 useRef 了,希望也能幫助到你唷!

最後,請注意 useRef 使用事項

Web Dev Simplified 的講者在其 useRef 教學影片中特別強調,雖然 useRef 有多種用途,但它並不應該過度取代 useState。特別是在處理用戶交互,如值的更改(onChange Value)時,過度依賴 useRef 可能會讓程式碼變得過於複雜。也可能讓你失去 React Hook 的渲染生命週期(re-render life cycle)所帶來的優勢。


💬 參考資料:

  1. Web Dev Simplified
  2. react hook 父调用子的方法 useRef