iT邦幫忙

2023 iThome 鐵人賽

DAY 20
0

昨天已經了解Uncontrolled Component和Controlled Component的差異及概念是什麼,也知道如果是自己客製化的元件,是可以依照情境將它設計為父層可以控制的Controlled Component,但是在實際的開發情境下,還是會有可能遇到需要深入控制Uncontrolled Component才能達到目的的情況,例如:使用的元件是外部的UI套件,這時候就不得不想辦法從父層去控制這個Uncontrolled Component,今天就讓我們從這樣的情境去看看可以怎麼做。

想要控制Uncontrolled Component!就要想辦法取得DOM元素

當子元件的值沒有透過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>

https://ithelp.ithome.com.tw/upload/images/20230924/20130914xEj3ZW3yMq.png

如果是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>
  );
}

https://ithelp.ithome.com.tw/upload/images/20230924/20130914CaiUUBNtsH.png

Vue和React的用法很像,但是Vue的ref是.value,而React的ref則是.current。像這樣子透過ref取得DOM之後,不管是要取得DOM元素的值或是對DOM元素進行操作都不是難事了,原本的Uncontrolled Component就變成了Controlled Component。不過除非真的沒有辦法從父層直接控制子層,否則不建議用這樣的方法操作,如果是自己客製化的元件,還是依照實際的需求設計成Controlled Component會比較好,因為這樣對於元件及相關資料流的掌控性才會是最高的狀態。

useRef可以取代useState?

剛剛看了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特性帶來的另一個用途 - 保存持久不變之值

雖然useRef沒有辦法用來管理與畫面渲染有關的state,但是useRef與useState不同的特性,也使得useRef有著與useState不同的用途。
https://ithelp.ithome.com.tw/upload/images/20230924/20130914a4fAazTHl7.png
當使用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;

https://i.imgur.com/kxqndMg.gif
在這個情境中可以觀察到不論重新渲染幾次,用useRef宣告的timerId的值都不會有變動。

想取得React元件的DOM使用forwardRef

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外還會看到一個警告。
https://i.imgur.com/tuVFn36.gif

在這裡我們可以看到實際實驗的結果,真的無法這樣直接存取到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;

https://i.imgur.com/TMJACGZ.gif

雖然Uncontrolled元件在一般的使用方法中,沒辦法進一步存取或控制,但是只要透過ref,還是可以進一步操控Uncontrolled元件。除了控制Uncontrolled元件之外,React的useRef還因為它的特性,使得它有保存永久不變之值的用途,雖然平常可能不太會用到useRef,但知道這個用法後,在實作中,也就能有更多更好的方法可以活用。

參考資料

useRef
forwardRef


上一篇
【Day 19】認識Controlled & Uncontrolled Component
下一篇
【Day 21】管理邏輯複雜的狀態 - useReducer
系列文
從Vue學React!不只要會用,還要真的懂~30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言