本文 AI 產出,尚未審核

React + TypeScript:forwardRef 型別定義完整指南


簡介

在 React 中,ref 提供了一條直接存取 DOM 元素或子元件實例的管道,常用於聚焦、測量尺寸或與第三方 UI 函式庫整合。當我們在 TypeScript 專案裡使用 ref 時,若不正確地為 forwardRef 產生型別,會導致編譯錯誤、IDE 智慧提示失效,甚至執行時出現不可預期的行為。

本篇文章聚焦於 React 的 forwardRef,說明如何在 TypeScript 中為它寫出正確且可維護的型別定義。從基礎概念到進階寫法,配合 4 個實務範例、常見陷阱與最佳實踐,幫助你在日常開發中安全、快速地使用 forwardRef


核心概念

1. 為什麼需要 forwardRef

forwardRef 允許父層元件把 ref 直接傳遞給子層的 DOM自訂元件,而不必透過額外的 props 轉發。這在以下情境特別有用:

  • 聚焦表單欄位:父元件想要在表單載入後自動聚焦某個 <input>
  • 與第三方 UI 函式庫整合:許多 UI 套件(如 Material‑UI、Ant Design)要求外部取得內部的 DOM 節點。
  • 高階元件 (HOC) 的透明化:HOC 包裝後仍希望保留原始元件的 ref 能力。

2. forwardRef 的基本型別

在 JavaScript 中,我們這樣寫:

import React, { forwardRef } from 'react';

const MyButton = forwardRef((props, ref) => (
  <button ref={ref} {...props}>Click me</button>
));

在 TypeScript 中,forwardRef 會返回一個泛型函式。最簡單的型別寫法如下:

import React, { forwardRef, ButtonHTMLAttributes } from 'react';

const MyButton = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
  (props, ref) => (
    <button ref={ref} {...props}>Click me</button>
  )
);
  • 第一個泛型參數 (HTMLButtonElement) 表示 ref 最終指向的 DOM 類型。
  • 第二個泛型參數 (ButtonHTMLAttributes<HTMLButtonElement>) 為 props 的型別。

重點:若省略這兩個泛型,ref 會被推斷為 any,失去型別安全。

3. React.RefReact.RefObject 的差別

型別 說明
React.Ref<T> 可以是 RefObject<T>、回呼函式 `(instance: T
React.RefObject<T> React.createRef<T>() 產生,擁有 .current 屬性。常在類別元件或函式元件裡使用。

forwardRef 中,我們通常不需要自行宣告 React.Ref<T>,只要在泛型裡提供 T(即最終的 DOM 或元件實例型別)即可。

4. 自訂元件的 forwardRef 型別

如果 ref 需要指向 子元件的實例(例如使用 useImperativeHandle 暴露方法),型別會稍微複雜一些。下面示範一個「可聚焦」的自訂 Input 元件:

import React, { forwardRef, useImperativeHandle, useRef } from 'react';

export interface FocusableInputHandle {
  focus: () => void;
}

const FocusableInput = forwardRef<FocusableInputHandle, React.InputHTMLAttributes<HTMLInputElement>>(
  (props, ref) => {
    const innerRef = useRef<HTMLInputElement>(null);

    // 把 focus 方法暴露給外層 ref
    useImperativeHandle(ref, () => ({
      focus: () => innerRef.current?.focus(),
    }));

    return <input ref={innerRef} {...props} />;
  }
);
  • forwardRef<FocusableInputHandle, ...>FocusableInputHandle 定義了外層可以呼叫的介面。
  • useImperativeHandle:將內部的 focus 方法映射到外部。

5. React.ForwardRefExoticComponentReact.RefAttributes

當我們把 forwardRef 包裝成 具備靜態屬性(如 displayName)的元件時,會得到 React.ForwardRefExoticComponent<P>

type MyCompProps = { label: string };
const MyComp = forwardRef<HTMLDivElement, MyCompProps>((props, ref) => (
  <div ref={ref}>{props.label}</div>
));

MyComp.displayName = 'MyComp';
// MyComp 的型別是 React.ForwardRefExoticComponent<MyCompProps & React.RefAttributes<HTMLDivElement>>
  • React.RefAttributes<T> 會自動加入 ref?: React.Ref<T> 的屬性,使得使用 MyComp 時能直接傳入 ref

程式碼範例

以下提供 四個實務範例,從最簡單的 DOM 轉發到自訂 hook 暴露方法,皆附上完整的型別說明與註解。

範例 1:最簡單的 forwardRef(Button)

import React, { forwardRef, ButtonHTMLAttributes } from 'react';

// 1️⃣ 指定 ref 指向的 DOM 類型與 props 型別
const SimpleButton = forwardRef<
  HTMLButtonElement,                         // ref 最終指向 <button>
  ButtonHTMLAttributes<HTMLButtonElement>   // props 繼承原生 button 屬性
>((props, ref) => {
  // 2️⃣ 將 ref 直接掛在 <button> 上
  return <button ref={ref} {...props} />;
});

export default SimpleButton;

使用方式

const btnRef = React.useRef<HTMLButtonElement>(null);
<SimpleButton ref={btnRef} onClick={() => btnRef.current?.focus()}>點我</SimpleButton>

範例 2:自訂 Input 並暴露 focus 方法

import React, {
  forwardRef,
  useImperativeHandle,
  useRef,
  InputHTMLAttributes,
} from 'react';

// 1️⃣ 定義外部可以呼叫的介面
export interface InputHandle {
  focus: () => void;
}

// 2️⃣ forwardRef 的泛型:<介面, props>
const CustomInput = forwardRef<InputHandle, InputHTMLAttributes<HTMLInputElement>>(
  (props, ref) => {
    const innerRef = useRef<HTMLInputElement>(null);

    // 3️⃣ 把 focus 方法映射到外層 ref
    useImperativeHandle(ref, () => ({
      focus: () => innerRef.current?.focus(),
    }));

    return <input ref={innerRef} {...props} />;
  }
);

export default CustomInput;

使用方式

const inputRef = React.useRef<InputHandle>(null);
<CustomInput ref={inputRef} placeholder="請輸入文字" />
<button onClick={() => inputRef.current?.focus()}>聚焦輸入框</button>

範例 3:在 HOC 中保留 forwardRef 的型別

import React, {
  forwardRef,
  ComponentType,
  PropsWithChildren,
  Ref,
} from 'react';

// 1️⃣ HOC 會接收一個具備 ref 的元件,並回傳同樣支援 ref 的元件
function withLoading<P, T>(WrappedComponent: ComponentType<P & React.RefAttributes<T>>) {
  const ComponentWithLoading = forwardRef<T, PropsWithChildren<P>>(
    (props, ref) => {
      const [loading, setLoading] = React.useState(false);

      // 假設有一些非同步操作
      const handleClick = async () => {
        setLoading(true);
        // ... async work
        setLoading(false);
      };

      return (
        <div>
          {loading && <span>載入中...</span>}
          {/* 2️⃣ 把 ref 直接傳給 WrappedComponent */}
          <WrappedComponent {...props} ref={ref as Ref<T>} />
          <button onClick={handleClick}>觸發載入</button>
        </div>
      );
    }
  );

  // 讓開發者在除錯時能看到正確的名稱
  ComponentWithLoading.displayName = `withLoading(${WrappedComponent.displayName || WrappedComponent.name})`;
  return ComponentWithLoading;
}

// ----- 使用範例 -----
type CardProps = { title: string };
const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => (
  <div ref={ref}>
    <h3>{props.title}</h3>
  </div>
));

export default withLoading(Card);

說明

  • ComponentType<P & React.RefAttributes<T>> 確保 WrappedComponent 接受 ref
  • forwardRef<T, PropsWithChildren<P>> 中的 T 為最終的 ref 型別,PropsWithChildren<P> 讓 HOC 仍能接受子元素。

範例 4:結合 React.memoforwardRef(最佳化)

import React, {
  forwardRef,
  memo,
  ButtonHTMLAttributes,
} from 'react';

// 1️⃣ 定義 Props,使用泛型延伸原生 button 屬性
type IconButtonProps = {
  icon: React.ReactNode;
} & ButtonHTMLAttributes<HTMLButtonElement>;

// 2️⃣ forwardRef + memo:先 forwardRef 再包 memo
const IconButton = memo(
  forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
    const { icon, children, ...rest } = props;
    return (
      <button ref={ref} {...rest}>
        {icon}
        {children}
      </button>
    );
  })
);

IconButton.displayName = 'IconButton';

export default IconButton;

使用方式

const btnRef = React.useRef<HTMLButtonElement>(null);
<IconButton
  ref={btnRef}
  icon={<i className="fa fa-search" />}
  onClick={() => console.log('clicked')}
/>

常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記指定 ref 的泛型 forwardRef 預設回傳 any,失去型別安全。 永遠forwardRef<T, P> 中提供 T(ref 指向的型別)。
useImperativeHandle 回傳的物件與介面不匹配 編譯器不會自動檢查回傳物件與 forwardRef 泛型是否一致。 為回傳物件寫明 介面(如 InputHandle),並在 forwardRef 中使用相同介面。
在 HOC 中遺失 ref HOC 常以 props 方式包裝子元件,若未使用 forwardRef,外層無法取得 ref。 在 HOC 中使用 forwardRef,並把 ref 正確傳遞給子元件 (ref as Ref<T>)。
memo 內部包 forwardRef 時忘記設定 displayName 開發工具的元件樹會顯示匿名函式,難以除錯。 forwardRefmemo 包裝的元件手動設定 displayName
refkey 同時傳入,導致 refnull React 會先處理 key 再處理 ref,若 key 改變會重新建立元件,舊的 ref 失效。 確保在列表渲染時 key 穩定,且不在同一個渲染循環內改變 key

最佳實踐

  1. 明確寫出所有泛型forwardRef<DOMOrComponent, Props>,讓 IDE 完整提示。
  2. 使用 React.RefAttributes<T>:在自訂元件的型別宣告時,直接擴充 RefAttributes,避免手動加入 ref?: React.Ref<T>
  3. useImperativeHandle 的回傳物件抽成介面,保持一致性與可重用性。
  4. 在測試中加入 ref 行為:使用 @testing-library/reactref 測試,確保未因重構失去功能。
  5. forwardRef 內部避免使用 any:即使暫時無法確定型別,也可使用 unknown 再逐步收斂。

實際應用場景

1. 表單元件庫(如 Ant Design、Material‑UI)

這類 UI 套件的每個輸入元件都會提供 ref,讓使用者在表單提交前自行觸發驗證或聚焦。透過正確的 forwardRef 型別,開發者在自己的表單封裝層仍能保有原始元件的 ref 能力。

2. 動畫與測量(React‑Spring、Framer‑Motion)

許多動畫函式庫需要直接存取 DOM 節點以計算尺寸或座標。使用 forwardRef 包裝的自訂元件,使得外層動畫容器能直接取得子元件的實際 DOM。

3. 可重用的「彈出式」元件

例如自訂的 ModalTooltip,內部會在開啟時自動聚焦第一個可互動元素。外層只需要傳遞 refModal,不必關心裡面的結構。

4. 高階元件 (HOC) 與裝飾器

在大型專案裡,常見「權限檢查」或「國際化」的 HOC。若原始元件支援 ref,HOC 必須保留這個特性,否則會破壞原本的聚焦或測量功能。


總結

  • forwardRef 是 React 提供的 ref 轉發 機制,配合 TypeScript 必須明確指定 ref 的目標型別 (HTMLDivElement、自訂介面等) 與 props 型別
  • 使用 forwardRef<T, P>React.RefAttributes<T>useImperativeHandle,可以在 函式元件 中安全、清晰地暴露方法或 DOM。
  • HOC、memo、以及自訂 UI 套件 中保留 ref 能力,需要把 forwardRef 包裝在最外層,並適當地轉型 ref as Ref<T>
  • 常見陷阱包括忘記泛型、介面不匹配、HOC 丟失 ref 等;遵循「永遠寫出完整泛型」與「介面化回傳物件」的最佳實踐,可大幅降低錯誤風險。
  • 實務上,forwardRef表單、動畫、彈窗、以及高階元件 提供了必要的底層存取能力,是打造可重用、可測試、易維護的 React+TypeScript 程式碼的關鍵。

掌握了上述概念與範例後,你就能在日常開發中自信地使用 forwardRef,為 React 應用注入更強的型別安全與彈性。祝你寫程式快樂,專案順利 🚀。