昨天已經了解Uncontrolled Component和Controlled Component的差異及概念是什麼,也知道如果是自己客製化的元件,是可以依照情境將它設計為父層可以控制的Controlled Component,但是在實際的開發情境下,還是會有可能遇到需要深入控制Uncontrolled Component才能達到目的的情況,例如:使用的元件是外部的UI套件,這時候就不得不想辦法從父層去控制這個Uncontrolled Component,今天就讓我們從這樣的情境去看看可以怎麼做。
當子元件的值沒有透過props曝露給父層操控時,就會讓這個元件變成是Uncontrolled元件。如同昨天提到的Uncontrolled之所以是Uncontrolled,那是因為那個元件的state是由那個元件自己的DOM保管。但是如果想要從外面控制它,就必須找到能碰到這個元件DOM的方式。這個方法也就是今天的主題-「ref
」。
說到ref這個字,寫過Vue的人應該馬上就可以想到,這個ref不就是Vue用來管理state的API嗎?的確是那個ref沒錯,不過稍微調整一下使用方式,就能讓ref有著存取DOM的功用。
直接上一段Vue的程式碼看看!
這是一個普通的input,當我們沒有特別使用v-model或綁定state在value上時,我們無法「直接」取得input的value。
<div>
<input type="text">
</div>
但是透過ref,我們就可以快速的接觸到DOM,進而對DOM操作或是取得它的值。
如果是Vue的話,需要先透過ref宣告響應的變數,再用ref把這個變數綁定上去。在template中透過ref屬性帶上state時,Vue會將DOM元素和這個ref變數關聯起來,我們也就能透過這樣的方式取得我們想要取得的DOM,進而對這個DOM進行操作。
<template>
<div>
<input type="text" ref="inputRef">
<button @click="handleClick">get DOM</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const inputRef = ref(null);
const handleClick = () => {
console.log(inputRef.value); // 取得DOM元素
console.log(inputRef.value.value); // 取得input value
};
</script>
如果是React的話,則是先透過useRef讓他回傳一個物件,再把回傳的物件賦予到DOM元件上的ref屬性,就可以讓ref和DOM元素被關聯起來,進而能取得到對應的DOM元素。
import { useRef } from 'react';
function App() {
const inputRef = useRef(null);
const handleClick = () => {
console.log(inputRef.current); // 取得DOM元素
console.log(inputRef.current.value) // 取得input value
};
return (
<div className="App">
<input type="text" ref={inputRef} />
<button onClick={handleClick}>get DOM</button>
</div>
);
}
Vue和React的用法很像,但是Vue的ref是.value,而React的ref則是.current。像這樣子透過ref取得DOM之後,不管是要取得DOM元素的值或是對DOM元素進行操作都不是難事了,原本的Uncontrolled Component就變成了Controlled Component。不過除非真的沒有辦法從父層直接控制子層,否則不建議用這樣的方法操作,如果是自己客製化的元件,還是依照實際的需求設計成Controlled Component會比較好,因為這樣對於元件及相關資料流的掌控性才會是最高的狀態。
剛剛看了vue的ref和React的useRef例子後,可能有些人會想「既然React的useRef看起來跟Vue的ref這麼像,那是不是可以取代useState的使用呢?」這時候就需要先回想一下,useState的作用是什麼?useState廣義來說是用來保存state,但如果狹義來說的話,主要是用來管理一個和渲染有關的state
,因為當呼叫useState回傳的setState函式時,就會通知React進行一連串跟畫面重新渲染的動作。那再回到今天的主角useRef身上,useRef雖然也可以用來保存一個值,就如同前面的例子,我們把useRef用來存取DOM,但是仔細觀察的話,應該可以發現到useRef並沒有提供任何可以觸發重新渲染的方式
,所以如果把useRef當作useState使用的話,當變更useRef的值時,並無法正常觸發畫面重新渲染。useRef雖然跟Vue的ref很像,不過由於Vue和React的渲染機制就不一樣,所以在並沒有辦法和Vue的ref一樣可以用來管理與畫面渲染有關的state。
雖然useRef沒有辦法用來管理與畫面渲染有關的state,但是useRef與useState不同的特性,也使得useRef有著與useState不同的用途。
當使用useRef來宣告state的時候,會回傳一個帶有current key的物件,這使得useRef的state是帶有mutable特型的值,這樣的特性也就讓useRef的值可以很穩定地被保存在同個記憶體位址的物件內,使得不管畫面怎麼重新渲染,都不會影響到useRef的值,也就讓useRef很適合拿來保存持久不變的值。另外,也由於它並沒有提供觸發重新渲染的函式(例如:setState),所以改動current的值,並不會觸發畫面重新渲染,也就適合用來管理與畫面渲染無關的值,例如:保存setInterval回傳timer ID。
import { useRef, useState, useEffect } from 'react';
function TimerComponent() {
const [second, setSecond] = useState(0);
const timerId = useRef(null);
console.log('render!', timerId.current);
useEffect(() => {
timerId.current = setInterval(() => {
setSecond((prevSecond) => prevSecond + 1);
}, 1000);
return () => {
if (timerId.current) {
clearInterval(timerId.current);
}
};
}, []);
return (
<div>
<p>Current Second: {second} </p>
</div>
);
}
export default TimerComponent;
在這個情境中可以觀察到不論重新渲染幾次,用useRef宣告的timerId的值都不會有變動。
React為了避免大家直接對DOM直接做操作的動作,如果是React元件的話,其實並沒有辦法直接使用useRef來存取DOM,例如像是以下的這個使用情境的話,其實useRef最後得到的值會是null。所謂的React元件,就是建構在React下的元件,不是單純的HTML tag。
這個是我們客製的React元件
const ReactComponent = () => {
return (
<h1>child component title</h1>
)
};
export default ReactComponent;
當我們直接使用在父元件,並且想要透過ref存取這個子元件時,會發現是undefined。
import { useRef } from 'react';
import ReactComponent from './ReactComponent';
const App = () => {
const getComponentDom = () => {
console.log(reactComponentRef.current);
};
return (
<div className="container">
<ReactComponent ref={reactComponentRef} />
<button onClick={getComponentDom}>get child component</button>
</div>
);
};
export default App;
除了只能得到undefined外還會看到一個警告。
在這裡我們可以看到實際實驗的結果,真的無法這樣直接存取到React元件的DOM,但是在實務情境中,偶爾還是會遇到不得不用useRef去存取DOM來做進一步操作的時候,在這樣的情境下,就可以先使用forwardRef把目標的React元件包起來,並透過ref讓它的DOM元素可以暴露給父層。
像這樣用forwardRef把component包起來,就能把DOM元素透過從父層傳遞過來的ref帶回父層。
import React from 'react';
// 用forwardRef把元件包起來
const ReactComponent = React.forwardRef((props, ref) => {
return (
// 把ref綁定在元件上
<h1 ref={ref}>child component title</h1>
)
});
export default ReactComponent;
把React元件這樣調整過後,就可以在父元件中存取到它。
import { useRef } from 'react';
import ReactComponent from './ReactComponent';
const App = () => {
const reactComponentRef = useRef();
const getComponentDom = () => {
console.log(reactComponentRef.current);
};
return (
<div className="container">
<ReactComponent ref={reactComponentRef} />
<button onClick={getComponentDom}>get child component</button>
</div>
);
};
export default App;
雖然Uncontrolled元件在一般的使用方法中,沒辦法進一步存取或控制,但是只要透過ref,還是可以進一步操控Uncontrolled元件。除了控制Uncontrolled元件之外,React的useRef還因為它的特性,使得它有保存永久不變之值的用途,雖然平常可能不太會用到useRef,但知道這個用法後,在實作中,也就能有更多更好的方法可以活用。