接下來要來聊一個有趣的 Hook,也就是 useRef
這個 Hook,這個 Hook 有什麼特別的功能呢?就讓我們瞧瞧吧!
前面這邊先提一下 useRef
這個 Hook 稍微有一點特別而且有趣,為什麼會特別且有趣呢?這邊先讓我們看一下 useRef
基本寫法
const ref = useRef(initialValue);
基本上 useRef
用法與前面的 Hook 都差不多,但是要稍微注意的事情是 useRef
會回傳一個物件,而這個物件會有一個 current
的屬性,這個 current
屬性就是我們要存放的值,這邊先來看一個簡單的範例
const App = () => {
const ref = React.useRef(0);
console.log(ref); // { current: 0 }
return (
<div>
</div>
);
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);
很有趣吧?useRef
明明傳入的是一個單純的 Number 0
,但是卻回傳一個 { current: 0 }
物件,那麼這是為什麼呢?這邊讓我們翻一下 React Hook 原始碼
function useRef<T>(initialValue: T): {current: T} {
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
workInProgressHook = createWorkInProgressHook();
const previousRef = workInProgressHook.memoizedState;
if (previousRef === null) {
const ref = {current: initialValue};
if (__DEV__) {
Object.seal(ref);
}
workInProgressHook.memoizedState = ref;
return ref;
} else {
return previousRef;
}
}
上面這一段稍微可能有一點複雜,所以我們先稍微精簡一下變成以下這樣
function useRef<T>(initialValue: T): {current: T} {
// ... 忽略其他程式碼
if (previousRef === null) {
const ref = {current: initialValue}; // 核心重點
// ... 忽略其他程式碼
return ref;
} else {
// ... 忽略其他程式碼
}
}
我們可以看到回傳物件的原因就是 React 處理的,因此這也是為什麼 useRef
會回傳一個物件。
除此之外 useRef
並不會觸發 re-render,因此可以拿來存放一些不會變動的值,但是你要注意一下,請不要這樣子撰寫
const App = () => {
let ref = React.useRef(0);
React.useEffect(() => {
ref += 1;
}, []);
return (
<div>
</div>
);
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);
如果你如上方這樣子撰寫的話,其實你 console.log(ref)
之後會看到 [object Object]1
,這樣子是錯誤的,因為 ref
是一個物件,所以你要這樣子撰寫才正確
const App = () => {
let ref = React.useRef(0);
React.useEffect(() => {
ref.current += 1;
console.log(ref); // { current: 1 }
}, []);
return (
<div>
</div>
);
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);
那麼這邊拉回到剛剛前面所提的「useRef
並不會觸發 re-render」這件事情,我們是如何知道畫面不會被重新 re-render 呢?其實驗證方式很簡單,將 ref
渲染在畫面上,然後寫個 setTimeout
去累加就可以知道畫面會不會 re-render
const App = () => {
const ref = React.useRef(0);
React.useEffect(() => {
setTimeout(() => {
ref.current += 1;
}, 2000)
}, []);
return (
<div>
{ ref.current }
</div>
);
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);
我們可以看到上述範例中,畫面並沒有被重新 re-render,因此我們可以確定 useRef
並不會觸發 re-render。
那麼因為這個特性關係,因此我們可以藉由此特性來監測畫面是否有被 re-render,而這邊我們必須搭配 useEffect
來使用,因為 useEffect
會在每次 re-render 後被觸發,所以這邊舉例一範例來說明。
const App = () => {
const [count, setCount] = React.useState(0);
const ref = React.useRef(0);
const fn =() => {
setCount((pre) => pre + 1);
}
const showRef = () => {
console.log(`當前畫面以 re-render ${ ref.current} 次`);
}
React.useEffect(() => {
ref.current += 1;
});
return (
<div>
<p>count:{ count }</p>
<button type="button" className="bg-blue-400 p-4 text-white hover:bg-blue-700" onClick={ fn }>點我</button>|
<button type="button" className="bg-blue-400 p-4 text-white hover:bg-blue-700" onClick={ showRef }>顯示已渲染的次數</button>
</div>
)
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);
我們可以看到上述範例中,當我們點擊「點我」按鈕時,畫面上的 count:0
會被重新 re-render 成 count:1
,隨著我們點擊幾次,畫面上就會跟著呈現相對應得 count
,而當我們點擊「顯示已渲染的次數」按鈕時,我們可以看到畫面已經被重新 re-render 了 N 次,因為 useEffect
會在每次 re-render 後被觸發,所以這邊我們可以藉由 useRef
來監測畫面是否有被 re-render,但你會發現不管怎麼樣 red.current 永遠都是比當前畫面的 count
多一次,這原因主要是發生在初始 React 所導致的。
已經看到大半篇的 useRef
介紹,但實質上來講感覺 useRef
好像在開發上很弱、很沒用對吧?所以這邊我們就來插個花,先來聊一下 Vue 的部分。
還記得你在初學 Vue 的時候都是如何選取 DOM 的嗎?忘了也沒關係,這邊提供範例程式碼讓你回憶一下
<div id="app">
<p class="p">Lorem ipsum dolor, sit amet consectetur adipisicing elit. Iure commodi consectetur cumque nesciunt dolorem deleniti ipsum atque modi libero consequuntur, porro labore autem quod! Voluptatibus eos eveniet optio nemo atque.</p>
</div>
const { createApp, onMounted } = Vue;
const app = createApp({
setup() {
onMounted(() => {
console.log('DOM:', document.querySelector('.p'));
})
}
});
app.mount('#app');
上面就是一個很簡單的 Vue 選取 DOM 方式。
但這種方式並不是很方便畢竟要寫很長的 document.querySelector
,所以 Vue 也提供了 ref
來幫助我們選取 DOM
<div id="app">
<p ref="p">Lorem ipsum dolor, sit amet consectetur adipisicing elit. Iure commodi consectetur cumque nesciunt dolorem deleniti ipsum atque modi libero consequuntur, porro labore autem quod! Voluptatibus eos eveniet optio nemo atque.</p>
</div>
const { createApp, onMounted, ref } = Vue;
const app = createApp({
setup() {
const p = ref(null);
onMounted(() => {
console.log('DOM:', p.value);
})
return {
p,
}
}
});
app.mount('#app');
兩者寫法攤開來看就可以看出兩者的差異,那麼接著拉回到 React 的部分。
相信你看到上面的範例之後,大概就很清楚 useRef
在實戰上還可以用於選取 DOM 了吧?所以就來示範一下吧
const App = () => {
const pRef = React.useRef(null);
React.useEffect(() => {
console.log('DOM:', pRef.current);
});
return (
<div>
<p ref={ pRef }>Lorem ipsum dolor sit amet consectetur adipisicing elit. Obcaecati ad assumenda laborum id alias? Reiciendis fugiat, explicabo, natus aperiam debitis facilis quia consequatur voluptatum veniam nobis nulla ad perspiciatis in. </p>
</div>
)
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);
除此之外 useRef
也可以用來選取元件,但是這邊我就不額外示範了,而是保留給你自己嘗試看看囉。
本文將會同步更新到我的部落格