終究都要學 React 何不現在學呢? - React 進階 - useRef - (16)

前言

接下來要來聊一個有趣的 Hook,也就是 useRef 這個 Hook,這個 Hook 有什麼特別的功能呢?就讓我們瞧瞧吧!

useRef

前面這邊先提一下 useRef 這個 Hook 稍微有一點特別而且有趣,為什麼會特別且有趣呢?這邊先讓我們看一下 useRef 基本寫法

1
const ref = useRef(initialValue);

基本上 useRef 用法與前面的 Hook 都差不多,但是要稍微注意的事情是 useRef 會回傳一個物件,而這個物件會有一個 current 的屬性,這個 current 屬性就是我們要存放的值,這邊先來看一個簡單的範例

1
2
3
4
5
6
7
8
9
10
11
12
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 原始碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
}

上面這一段稍微可能有一點複雜,所以我們先稍微精簡一下變成以下這樣

1
2
3
4
5
6
7
8
9
10
function useRef<T>(initialValue: T): {current: T} {
// ... 忽略其他程式碼
if (previousRef === null) {
const ref = {current: initialValue}; // 核心重點
// ... 忽略其他程式碼
return ref;
} else {
// ... 忽略其他程式碼
}
}

我們可以看到回傳物件的原因就是 React 處理的,因此這也是為什麼 useRef 會回傳一個物件。

除此之外 useRef 並不會觸發 re-render,因此可以拿來存放一些不會變動的值,但是你要注意一下,請不要這樣子撰寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 是一個物件,所以你要這樣子撰寫才正確

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 後被觸發,所以這邊舉例一範例來說明。

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
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 的嗎?忘了也沒關係,這邊提供範例程式碼讓你回憶一下

1
2
3
<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>
1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 了吧?所以就來示範一下吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 也可以用來選取元件,但是這邊我就不額外示範了,而是保留給你自己嘗試看看囉。

後記

本文將會同步更新到我的部落格