# [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]); // ... } ``` ![CleanShot 2024-01-07 at 21.05.55](https://hackmd.io/_uploads/ByXyf7uup.gif) (可以發現確實有執行 `handleClickRef` 中的 log) ![CleanShot 2024-01-07 at 21.09.31](https://hackmd.io/_uploads/H1aoGmdup.gif) (但是當我由 `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)