React.js
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 指向的東西就會不一樣)

也就是說剛剛的「理想」只要引入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 的時候又會被重新初始化,就無法達到計數的效果。

  • setTimeout、setInterval、removeTimeout、removeInterval

    因為要 reference 一樣才能正常移除函式,但這個也可以用 useCallback 做(後面會講這個是啥)。雖然沒有特別規定,不過有人會認為 useCallback 在閱讀時會更直覺聯想到是函式。

  • 避免 useEffect 在建立元素時被執行

const mounted = useRef();
useEffect(() => {
    if (mounted.current === false) {
        mounted.current = true;
        /* 下面是 componentDidMount*/
 
        /* 上面是 componentDidMount */
    } else {
        /* 下面是componentDidUpdate */
        /* 上面是componentDidUpdate */
    }
 
    return () => {
        /* 下面是 componentWillUnmount */
        /* 上面是 componentWillUnmount */
    };
}, [dependencies參數]);