今天要來介紹的是useRef這個hook,在使用的時候可能常常會和useState搞混,但是事實上它們的使用情境是很大的不同的,接下來來一一介紹useRef的特性與使用方式。
const ref = useRef(initialValue)
使用useRef會回傳一個物件,帶有唯一一個屬性current
並賦予一開始帶入的initialValue
,接下來要變更值的時候可以直接進行修改,ref.current = 變更值
,還有個很重要的地方就是,更新值後並不會像useState的setter function一樣觸發重新渲染,而且也不建議在元件渲染時讀寫current
,這是因為變更current
不會觸發渲染,所以在渲染得到的值可能跟你想像的會不太一樣。與useStatey的差別比較表如下
useRef | useState | |
---|---|---|
回傳值 | 回傳一個帶有current屬性的物件,current的值為帶入的useRef的值 | 回傳一個帶有值和setter function的陣列 |
更新方式 | 直接變更current的值 | 使用setter function |
渲染 | 變更值不會重新渲染 | 使用setter更新值後重新渲染 |
盡可能不要在渲染的時候變更值 | 可以在渲染的時候變更值 |
我們可以看以下這個簡單的範例,當current + 1的時候console出來的數字有持續+1,但是畫面上就是沒有改變的,就是因為這個值變更了不會讓元件重新渲染。
export default function MyApp() {
const ref = useRef(0);
return (<>
<button onClick={() => {
ref.current = ref.current + 1
console.log(ref)
}}>
click to + 1
</button>
{ref.current}
</>);
}
useRef回傳的物件就如同Js的物件一樣沒有像是setter function那樣重新渲染的功能,它就像是React提供一個跳脫React狀態更新並儲存值的一個方式,由於擁有不會重新渲染的特性,所以適用於不需要畫面更新的功能,像是
const ref = useRef();
ref.current = setTimeout(() => {}, 1000);
clearTimeout(ref.current); // 可以用來儲存timeout id,以便之後可以clear
在JSX的屬性ref帶入useRef的回傳值,就可以得到DOM node
const inputRef = useRef();
<input ref={inputRef} />
// inputRef.current可以取得input DOM
取得input並且focus
import {useRef} from 'react'
export default function MyApp() {
const inputRef = useRef(null);
return (<>
<button onClick={() => {
inputRef.current.focus()
}}>focus input</button>
<input type="text" ref={inputRef} />
</>
);
}
雖然建議不要在渲染的時候讀寫useRef值,但是在某些情形下使用是合理的,但要注意的是確保每次回傳的結果是相同的 ,如下範例playerRef.current都會回傳一個video。
function Video() {
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
//...
}
如果要操作數個element,例如我們想要拿到所有ul底下li的DOM node,我們無法用for迴圈的方式來達到,因為hook只能在最上層使用不能用在條件或是迴圈內。
// x 錯誤範例
<ul>
{items.map((item) => {
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>
除了手動宣告一堆useRef並且一個一個加在ref屬性外,我們可以使用JSX的ref屬性的ref callback來達到拿到數個DOM node的效果。直接看官方的文件範例,它在ref帶入一個function,並且帶有一個該element的node參數,可以透過這個callback將個別li的node分別存到useRef賦予的map裡。
import { useRef } from 'react';
export default function CatFriends() {
const itemsRef = useRef(null);
function scrollToId(itemId) {
const map = getMap();
const node = map.get(itemId);
node.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
function getMap() {
if (!itemsRef.current) {
// Initialize the Map on first usage.
itemsRef.current = new Map();
}
return itemsRef.current;
}
return (
<>
<nav>
<button onClick={() => scrollToId(0)}>
Tom
</button>
<button onClick={() => scrollToId(5)}>
Maru
</button>
<button onClick={() => scrollToId(9)}>
Jellylorum
</button>
</nav>
<div>
<ul>
{catList.map(cat => (
<li
key={cat.id}
ref={(node) => {
const map = getMap();
if (node) {
map.set(cat.id, node);
} else {
map.delete(cat.id);
}
}}
>
<img
src={cat.imageUrl}
alt={'Cat #' + cat.id}
/>
</li>
))}
</ul>
</div>
</>
);
}
const catList = [];
for (let i = 0; i < 10; i++) {
catList.push({
id: i,
imageUrl: 'https://placekitten.com/250/200?image=' + i
});
}
會有兩個時機呼叫這個function,就如同在更新畫面的時候會經過兩個階段,一個是渲染的時候,另個是node實際畫到畫面上的時候(commit)都會呼叫callback。但只有commit那一次帶的參數是實際的node,它會在畫上畫面之後馬上呼叫,但渲染那次得到的是null,跟文件的理念一致不在渲染的時候存取useRef值,實際上在渲染階段也還沒有拿到實際的node。
在預設我們自己定義的元件是不允許ref這個屬性,會出現以下的錯誤。
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
// 錯誤範例
function MyComponent({ref}) {
return <input type="text" ref={ref} /> // 其實只要prop不是ref這個名稱就可以用,但是這不是個好方法
}
function App() {
const inputRef = useRef(null);
return <MyComponent ref={inputRef}/>
}
會有這樣的錯誤是,React認為元件如果暴露它的的ref提供存取,會讓的code變得很脆弱容易有問題,於是如果需要這種功能的話就需要opt in的方式,將ref給開放出來供其他元件使用。
使用forwardRef
帶入一個元件參數,元件帶有兩個參數,第一個是props,第二個參數是傳入的ref
const SomeComponent = forwardRef(render)
import { forwardRef } from 'react';
const MyInput = forwardRef(({ value, onChange }, ref) => {
return (
<input
value={value}
onChange={onChange}
ref={ref}
/>
);
});
export default MyInput;
如果forwardRef搭配useimperativehandle使用可以指定要暴露給其他元件存取的方法。如同以下範例,只有開方focus方法並且自己撰寫實際執行的程式。這時候在父層使用ref.current.focus
就會執行在useImperativeHandle裡面定義好的focus function。
import { forwardRef, useRef, useImperativeHandle } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
},
};
}, []);
return <input {...props} ref={inputRef} />;
});
https://react.dev/reference/react/useRef
https://react.dev/learn/referencing-values-with-refs
https://react.dev/learn/manipulating-the-dom-with-refs