前端框架

-

useRef 教學 - React 存取 DOM 與保持資料一致的 Hook

this.web

useRef 教學文章封面

什麼是 useRef

useRef 是 React 提供的一個 Hook,它可以用來保存一個值,而這個值會被存在一個物件裡面,這個物件再更新組件時,會保持一致的資料,不會被清空。

因為會被保存在物件中,而物件在 JS 裡是參考值,所以這個 Hook 才叫做 useRef (Reference 參考值)。

useRef 的語法

useRef 的語法很簡單:

import { useRef } from 'react';

function MyComponent() {
  const myRef = useRef(initialValue);
  // ... 其他程式碼
}

要注意的是,useRef(initialValue) 會返回一個只有單一屬性 current 的物件。也就是說如果你要使用或更改裡面的值,你必須這樣做:

myRef.current = 'otherValue';

console.log(myRef.current); // otherValue

這個 current 初始時會被設定為你傳入的 initialValue(可以是任意類型的值),而且這個初始值只在第一次 render 時有效

後續的重新渲染中,useRef 都會回傳同一個物件,也就是說 ref.current 在組件生命週期內會持續保存前一次更新後的值,而不會每次重置。

useRef 和 useState 的差別

是否觸發組件重新渲染

useRefuseState 都是用來儲存值的,他們不同的地方在於:useRef 的值改變不會觸發組件的重新渲染。

當我們修改 useRef 的值時,React 不會因為我們修改就更新組件,這代表 useRef 非常適合儲存那些**不影響畫面的資料,例如計時器的 ID、歷史紀錄等等。**例如下面這個計時器的範例:

import React, { useState, useRef, useEffect } from 'react';

function SimpleTimer() {
 const [seconds, setSeconds] = useState(0);

 // 使用 useRef 儲存計時器 ID,修改時不會觸發重新渲染
 const intervalRef = useRef(null);

 // 用來儲存 interver 的 id,並在卸載時清除
 useEffect(() => {
   intervalRef.current = setInterval(() => {
     setSeconds(prevSeconds => prevSeconds + 1);
   }, 1000);

   return () => {
     if (timerRef.current) clearInterval(timerRef.current);
   };
 }, []);

 return <p>已運行: {seconds}</p>
}

同步與非同步

除此之外,useRef 的更新是會馬上生效的:

const countRef = useRef(0);

function handleClick() {
  countRef.current += 1;
  console.log(countRef.current); // 1 - 立刻得到最新值
}

而當我們使用 useState,其實只是告訴 React 說要排程一次更新,接著 React 會先搜集要更新的 state,並統一更新。這代表 useState 值的更新是會稍微延遲的:

const [count, setCount] = useState(0);

function handleClick() {
  setCount(prev => prev + 1);
  console.log(count); // 0 這裡拿到的是舊值,因為還沒重新渲染
}

延伸閱讀:batch update 是什麼?React 非同步的狀態更新。

useRef 的實際運用

由於 useRef 可以在多次渲染中抱持值的一致,所以很適合用來儲存 DOM 元素,也是最常見的應用場景。如果我們想獲取某個特定元素以及他的資料,就可以這樣用:

import { useRef, useEffect } from 'react';

function Comp() {
  const pRef = useRef(null);

  useEffect(() => {
    console.log(pRef.current?.textContent); // Hello World
  }, []);

  return (
    <p ref={pRef}>
      {/* 透過 ref={pRef} 獲取特定 DOM 元素 */}
      Hello World
    </p>
  );
}

再舉一個例子,如果我們想控制一個影片的播放暫停,我們也可以使用 useRef,並將 videoRef 傳給 video,接著就能控制影片

import { useRef } from 'react';

function VideoPlayer() {
  const videoRef = useRef(null);

  const play = () => videoRef.current.play();
  const pause = () => videoRef.current.pause();

  return (
    <>
      <video ref={videoRef} width="320" src="video.mp4" />
      <button onClick={play}>播放</button>
      <button onClick={pause}>暫停</button>
    </>
  );
}

除了用來抓取 DOM,將 useRef 用來儲存資料也非常實用,因為他不會讓 react re-render 組件,所以在某些場景可以優化效能,例如我想要儲存滑鼠的位置但滑鼠位置不影響畫面的更新,就可以使用 useRef 而不是 useState

import { useRef, useEffect } from 'react';

export default function MousePositionRef() {
  const mousePosRef = useRef({ x: 0, y: 0 });

  useEffect(() => {
    const handleMove = e => {
      mousePosRef.current = { x: e.clientX, y: e.clientY };
    };
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);

  const showPosition = () => {
    const { x, y } = mousePosRef.current;
    alert(`目前滑鼠位置:(${x}, ${y})`);
  };

  return <button onClick={showPosition}>顯示滑鼠位置</button>;
}

forwardRef 和 useRef 的搭配

在 React 19 之前,如果我們想將 ref 傳給從父層往下傳遞,需要搭配 forwardRef 使用,如果單純當作 props 往下傳是沒有用的,例如這樣**:**

// ❌ 錯誤使用方式
function Parent() {
  const pRef = useRef(null);

  return <Child ref={pRef} />
}

function Child({ref}) {
  return <p ref={ref}>Hello World</p>
}

這個時候需要搭配 forwardRef 將 Ref 傳遞給子組件:

// ✅ 正確使用方式
function Parent() {
  const pRef = useRef(null);

  return <Child ref={pRef} />
}

const Child = forwardRef((props, ref) => {
  return <pRef ref={ref}>Hello World</p>
})

不過在 React 19 之後,就不需要使用 forwardRef 了,直接傳 ref 就好,但維護舊專案時就要注意了。

如何在 TypeScript 中正確使用 useRef

在使用 TypeScript 時,useRef 也需要一些額外的注意,尤其在型別註記初始值方面。

為 DOM 元素設定正確的型別:當我們用 useRef 來存放 DOM 元素的引用時,要在泛型中指定對應的元素型別。例如,HTMLInputElementHTMLDivElement 等。

通常我們會將初始值設為 null因為在尚未掛載前沒有 DOM 元素可引用。例如:

const inputRef = useRef<HTMLInputElement>(null);

這樣,inputRef.current 的型別就會是 HTMLInputElement | null

結語

我們詳細介紹了 React useRef Hook 的用法。對初學者而言,重點在於了解 useRef 可以跨渲染儲存資料不會引起重渲染,常常用來訪問 DOM 或保存一些輔助性資訊。

所以什麼資料適合放進 ref,什麼資料該用 state,很重要,使用的很可以更優化效能。

你可能會感興趣的文章 👇