# [React] useRef
###### tags `React` `前端筆記`
<div style="display: flex; justify-content: flex-end">
> created: 2024/01/02
</div>
> `useRef` is a React Hook that lets you reference a value that’s not needed for rendering.
## `useRef`
叫用 `useRef(initialValue)` 後會得到一個物件 `{ current: initialValue }`,之後取得值的話就是透過 `ref.current` 取得。
隨後的 renders React 會確保每次都會得到==相同的物件==(`{ current: ... }`)。
:::info
我們更改的只是 `ref.current` 中 `current` 屬性的值,而不是改變 ref 整個物件。
:::
## 用途
與 `useState` 不同,即便我們更新 `useRef.current` 中的值也不會讓元件重新渲染,所以有 UI 相關的資料別用 `useRef` 保存(因為不會觸發元件重新叫用,所以 UI 不會更新)
但是,在下列時機點 `useRef` 還是有使用的時機:
1. 抓取 DOM 做事
```javascript!
const myRef = useRef(null)
/* 當 div 繪製到 DOM 時,React 會將此 div 放入 current 的值,且當該 div 移除 DOM 時,React 會將 myRef.current 設為 null */
return (
<div ref={myRef} />
)
```
2. 與第三方套件整合
```javascript!
// 但要注意,當元件重新叫用時(裡面的程式碼重新跑一遍),還是會叫用建立實例的函式,因此可以額外判斷是否要重新建立實例
function Video() {
/* 每次元件重新叫用時,都會執行 new VideoPlayer() */
const playerRef = useRef(new VideoPlayer());
// ...
}
function Video() {
const playerRef = useRef(null);
/* 額外判斷建立實例的時機,避免重複建立實例 */
if (playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
// ...
}
```
3. 保存不需要渲染的資料(比如說透過記錄 timer 達到 debounce 的效果)
```javascript!
// ref. https://react.dev/learn/referencing-values-with-refs
// 處理使用者點擊 dedounce 行為
import { useRef } from 'react'
function DebouncedButton({ onClick, children }) {
const timeoutID = useRef()
return (
<button onClick={() => {
if (timeoutID.current) {
clearTimeout(timeoutID)
}
timeoutID.current = setTimeout(() => {
onClick();
}, 1000);
}}>
{children}
</button>
);
}
```
## 需要注意的地方
因為程式碼是一行一行執行的,所以要注意取用 `ref.current` 時的時機點:
### 1. 在 rendering 期間(也就是執行元件內的程式碼至渲染到 DOM 前)別更改或讀取 `ref`
```javascript!
// ref. https://react.dev/reference/react/useRef
function MyComponent() {
// ...
// 🚩 Don't write a ref during rendering
myRef.current = 123;
// ...
// 🚩 Don't read a ref during rendering
return <h1>{myOtherRef.current}</h1>;
}
```
### 2. 於 `useEffect` 的 dependency array 放入 `ref.current` 時,更新 `ref.current` 後並不會觸發 effect
以下方程式碼為例,當使用者觸發 `handleClickRef` 更改 `ref.current` 後**並不會叫用 effect**:
```javascript!
function App() {
const [count, setCount] = useState(0);
const ref = useRef(0);
const handleClick = () => {
setCount((prev) => prev + 1);
ref.current = ref.current + 1;
};
const handleClickRef = () => {
ref.current = ref.current + 1;
console.log(ref.current);
};
useEffect(() => {
console.log(ref.current);
console.log("effect");
}, [ref.current]);
// ...
}
```

(可以發現確實有執行 `handleClickRef` 中的 log)

(但是當我由 `handleClick` 更新 state 及 `ref.current` 後,effect 確實有執行了)
#### 回到原則
1. `ref` 更改不會觸發元件重新 re-render(也就是叫用)
2. 每一次元件重新 re-render(也就是叫用)都是自己的範疇,所以有自己的一切(`state`, `props`, `effects` ...)
所以當我們只有更新 `ref.current` 時,元件並不會重新 re-render,我們只是像是更改物件其中的屬性一樣,直接替換 `ref.current` 的值:
```javascript!
/* 第一次 render */
const ref = {
current: 0
}
useEffect(() => {
// ...
}, [0])
/* 透過 handleClickRef 更新 ref.current */
const ref = {
current: 1
}
/* 因為更換 ref.current 並不會觸發元件 re-render,所以 effect 自然不會更新,因為尚未滿足 effect 觸發的條件(若元件重新渲染後,當 dependency array 中有與前一次不同時,在新的範疇 DOM 渲染後就會叫用 effect)*/
```
但當我們透過 `handleClick` 額外更新 state 後,就會有些許不同了:
```javascript!
/* 第一次 render */
const ref = {
current: 0
}
useEffect(() => {
// ...
}, [0])
/* 透過 handleClick 更新 ref 及 count */
const ref = {
current: 1
const count = 1
/* 更新 state,造成元件重新 re-render,所以也會有屬於新範疇的 useEffect,這時 useEffect 就會知道此次的 dependency array 與上一次不同,就會執行 effect */
useEffect(() => {
// ...
}, [1])
```
:::info
這種將 `ref.current` 放入 dependency array 被視作一種 anti-pattern。
*[ref. Is it safe to use ref.current as useEffect's dependency when ref points to a DOM element?](https://stackoverflow.com/questions/60476155/is-it-safe-to-use-ref-current-as-useeffects-dependency-when-ref-points-to-a-dom)*
:::
[程式範例](https://codesandbox.io/p/devbox/useref-with-useeffect-8yqrg2?file=%2Fsrc%2FApp.tsx)
## Recap
1. `ref` 就是一個物件,只有一個屬性 `current`,所以讀取寫入就是直接針對 `ref.current` 操作
2. 更新 `ref.current` 並不會讓元件重新 re-render,但是 `ref.current` 還是會更新
3. 每次 re-render 元件都有屬於自己的 scope,該 scope 存放著屬於該 scope 的 everything (`props`, `state`, `handlers`, `effects` ...)
4. 勿在 rendering 期間寫入或者讀取 `ref.current`
## 參考資料
1. [useRef](https://react.dev/reference/react/useRef)
2. [Ref objects inside useEffect Hooks](https://medium.com/@teh_builder/ref-objects-inside-useeffect-hooks-eb7c15198780)
3. [Referencing Values with Refs](https://react.dev/learn/referencing-values-with-refs)