iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 15
1
Modern Web

從比入門再往前一點開始,一直到深入React.js系列 第 15

【Day.15】React入門 - 非控制組件與useRef

reference,中文翻譯是「參考」。聽起來好像有點奇怪,但他在程式中一般是指「變數指向的記憶體位置上對應到的值」。

超級複雜的啦。

簡單來說可以想像成是房子跟地址的關係。記憶體就像是地址,變數對應的值就像是房子,「沿著地址找到房子」這個過程就是reference。房子本身可能會有很多內部變動,但不管怎麼變,房子所在的地址是不變的。

在Javascript變數中,物件和Array一般會是以類似reference的方式來傳遞,其他的變數通常會複製一份後,把複製出的那一份拿來傳遞。

更正確的原理可以參考Huli大大的文章:

reference和非控制組件的關係

當程式大起來,網頁中的元素很多,當想要用原始DOM api去操作元素時,卻還要用document.querySelector或是document.getElementById去整個網頁找,就顯得很不直覺。

能不能直接在JSX中取得元素的reference,直接操作元素本身呢 ?

也就是說,理想上我們希望做這種事情

用一個變數去綁在元素的props上,然後就能讓該變數等於綁定元素的reference

大概是這樣(實際上當然不能直接這樣做):

import React, {useRef} from "react";

const InputForm=()=>{
    let accountRef = {};
    let passwordRef = {};

    let refArr = [accountRef,passwordRef];

    return (
        <>
            <input 
                type="text" 
                name="account"
                ref={accountRef} 
            />
            <input 
                type="text" 
                name="password"
                ref={passwordRef} 
            />
            <button onClick={()=>{
                refArr.forEach((item)=>{
                    console.log(item.name+" is "+item.value);
                })
            }}>提交</button>
        </>
    )
}  
export default InputForm;    

過去,React在class component中的確有提供React.createRef()這個API來創造一個可以讓你綁在ref這個props上的object變數。讓你能直接拿到該元素本身、直接用原始DOM方式操作元素。

但是這個API如果直接拿到function component來用會有問題。原因是React.createRef();通常只會在class component的建構子呼叫一次,這樣就能確保這個創造出來的reference指向的是同一個地址。然而function component沒有建構子,每次都一定會重新呼叫function component的定義域,這樣等於每次都會重新創造一次這個object變數,賦予值被重新初始化,指向的reference也會不一樣了

為了解決這個問題,React提供了另一個React hook - useRef

useRef

useRef是一個函式,跟useState一樣接收一個參數,作為變數初始值。差別是useRef回傳的是一個物件,裡面只有一個屬性current:

const data = useRef("初始資料")
console.log(data)

// { current: "初始資料" }

React會確保useRef回傳出來的這個物件不會因為React元件更新而被重新創造。也就是說在你初始化過後,這個物件會始終指向同一個reference。

請注意雖然物件本身指向位置一樣,但如果你重新assign物件中current屬性裡面的值,那current對應的value指向的東西就會不一樣。

也就是說剛剛的「理想」只要引入useRef後,只要先創造要綁在input的propsref上的變數,綁定之後,變數名稱.current就會是該input元素本身,我們就能用直覺的方式操作DOM元素了!

// 引入useRef
import React, {useRef} from "react";

const InputForm=()=>{
    // 建立用來綁定input的變數
    const accountRef = useRef(undefined);
    const passwordRef = useRef(undefined);

    // 為了方便操作,建立一個array來管理這些ref
    const refArr = useRef([accountRef,passwordRef]);

    return ( // 將剛剛創立的變數綁在對應的位置
        <>
            <input 
                type="text" 
                name="account"
                ref={accountRef} 
            />
            <input 
                type="text" 
                name="password"
                ref={passwordRef} 
            />
            <button onClick={()=>{
                refArr.current.forEach((item)=>{
                    console.log(item.current.name+" is "+item.current.value);
                })
            }}>提交</button>
        </>
    )
}
export default InputForm;

useRef的應用

由於useRef「不會因為update元件而被改變reference」的特性,讓其常被用在這些地方:

  • 以原生方式操作DOM元素

    上面講過了

  • counter變數

    如果用一般變數來當counter,元件被update的時候又會被重新初始化,就無法達到計數的效果。

  • addEventListener(removeEventListenser)、setTimeout(clearITimeout)、setInterval(clearInterval)。可以參考我去年的範例 【React.js入門 - 23】 元件練習(下) - 在function利用useEffect遞迴+useState實作動畫

    因為要reference一樣才能正常移除函式,但這件事在callback函式不需要和state/props有關時也可以用useCallback做(後面會講這個是啥)。雖然沒有特別規定,不過有人會認為useCallback在閱讀時會更直覺聯想到是函式。但是如果你的callback函式需要和state/props有關時,就要用useEffect搭配useRef實作。

  • 避免useEffect在建立元素時被執行
    也就是某些情況下,因為只會希望元件update時才有side Effect,所以需要一個變數來記憶是否為第一次渲染。
    const mounted=useRef(false);
    useEffect(()=>{
      if(mounted.current===false){
        mounted.current=true;
        /* 下面是 第一次渲染後 */
    
    
        /* 上面是 第一次渲染後 */      
      }
      else{
        /* 下面是元件更新後 */
    
    
        /* 上面是元件更新後 */

      }
      
      return (()=>{
           /* 下面是元件移除前 */
      
      
          /* 上面是元件移除前 */
      })
    },[dependencies參數]);

上一篇
【Day.14】React入門 - 輸入元素與控制組件
下一篇
【Day.16】React入門 - 想要分頁? react-router-dom
系列文
從比入門再往前一點開始,一直到深入React.js30

尚未有邦友留言

立即登入留言