React 19で、forwardRefを使わずにrefを受け取る方法

​​React​forwardRef,ref,TypeScript,useImperativeHandle

React 19 では、関数コンポーネントが ref をほかの props と同じように受け取れます。そのため、新しく書くコンポーネントでは forwardRef で包む必要がなくなりました。forwardRef 自体は React 19 でも動き、将来のバージョンで非推奨になる予定です。

筆者は検証環境で、同じ TypeScript(.tsx)のコンポーネントを React 19 と React 18 の両方で動かし、親から渡した ref がどこにつながるかを確かめました。その結果と、公式ドキュメントに書かれている変更点をまとめます。

補足

検証環境は Ubuntu 24.04、Node.js 22.22.0 です。React 19 側は react / react-dom 19.3.1、@types/react 19.3.0、typescript 7.0.2、比べる React 18 側は react / react-dom 18.3.1 です。どちらも esbuild 0.28.2 で変換し、jsdom 29.1.1 の上で描画しました。.tsx は tsc で型チェックしています。GIF は Playwright 1.56.1 の Chromium で撮りました。

React 19 では ref を props から取り出すだけ

React 19 の発表記事によると、React 19 からは関数コンポーネントで ref を props として読めます。検証では、次の MyInput を用意しました。forwardRef は使わず、分割代入で ref を取り出して <input> に渡すだけです。

tsx
import type { Ref } from 'react';

type Props = { placeholder?: string; ref?: Ref<HTMLInputElement> };

export function MyInput({ placeholder, ref }: Props) {
  return <input placeholder={placeholder} ref={ref} />;
}

親から ref を渡して描画し、ref.current の中身と、ref.current.focus() で入力欄にフォーカスが移ったかを調べました。同じファイルを React 18 でも変換して動かしています。

text
react 19.3.1
MyInput: ref.current=<input> focused=true
text
react 18.3.1
MyInput: ref.current=null focused=false
  console.error: Warning: MyInput: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)
  console.error: Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

React 19 では、ref.current が <input> になり、フォーカスも移りました。React 18 では ref.current は null のままで、「ref は props ではない」「関数コンポーネントには ref を渡せない。forwardRef を使うつもりか」という2つの警告が出ました。

同じ MyInput を Chromium で動かし、ボタンを3回押して ref.current.focus() を呼んだ様子がこちらです。上が React 18、下が React 19 です。

同じMyInputをReact 18と19で縦に並べ、フォーカスのボタンを3回押したときのref.currentとフォーカスできた回数を示す画面
フォーカスのボタンを3回押したときの動き。React 18 は ref.current が null のまま0回、React 19 は入力欄にフォーカスが移り3回になる

forwardRef は React 19 でもそのまま動く

これまでの書き方は、forwardRef の2つ目の引数で ref を受け取る形でした。

tsx
import { forwardRef } from 'react';

type Props = { placeholder?: string };

export const MyInputForwardRef = forwardRef<HTMLInputElement, Props>(
  function MyInputForwardRef({ placeholder }, ref) {
    return <input placeholder={placeholder} ref={ref} />;
  },
);

このコンポーネントは、React 19.3.1 でも React 18.3.1 でも ref.current=<input> focused=true になりました。React 19.3.1 の開発用ビルドで動かしても、forwardRef を使ったことへの警告は出ませんでした。

forwardRef のリファレンスの冒頭には、「React 19 では forwardRef はもう必要ない。代わりに ref を props として渡す」「forwardRef は将来のリリースで非推奨になる」と書かれています。発表記事も、将来のバージョンで非推奨にして取り除くとしています。今すぐ書き換えなくても壊れませんが、新しく書くコンポーネントでは使わないほうがよい、という位置づけです。

書き換えには、公式の react-codemod に remove-forward-ref という codemod があります。README に載っているコマンドは npx codemod react/19/remove-forward-ref --target <path> です。検証環境では codemod のレジストリからパッケージを取得できず、実行できませんでした。そのため、変換結果は確かめていません。

ポイント

React 18 では、ref を props で受け取る書き方は動きません。React 18 でも使われるライブラリのコンポーネントは、forwardRef を残す必要があります。

useImperativeHandle と memo でも ref は props で届く

DOM そのものではなく、決めたメソッドだけを親に見せたいときは useImperativeHandle を使います。useImperativeHandle のリファレンスは、React 19 からは props で受け取った ref をそのまま渡す書き方になっています。

tsx
import { useImperativeHandle, useRef, type Ref } from 'react';

export type FancyInputHandle = { focus: () => void; clear: () => void };

export function FancyInput({ ref }: { ref?: Ref<FancyInputHandle> }) {
  const inputRef = useRef<HTMLInputElement>(null);
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    clear: () => {
      if (inputRef.current) inputRef.current.value = '';
    },
  }), []);
  return <input ref={inputRef} defaultValue="hello" />;
}

React 19 では、親の ref.current が { focus, clear } のオブジェクトになり、focus() でフォーカスが移り、clear() で入力欄が空になりました。React 18 では ref.current は null でした。

memo で包んだコンポーネント(memo(function MemoInput({ placeholder, ref }: Props) { ... }))も試しました。React 19 では ref.current=<input> になり、React 18 では null でした。memo で包んでも、React 19 なら ref は props として中のコンポーネントに届きます。

TypeScript では ref を props の型に書く

関数コンポーネントで ref を受け取るときは、props の型に ref?: Ref<HTMLInputElement> のように書きます。<input> の props をまとめて受け取る ComponentProps<'input'> を使う場合は、自分で書き足さなくても、型に ref が入っています。検証では、props: ComponentProps<'input'> で受け取って <input {...props} /> に渡すだけのコンポーネントで、React 19 では ref.current=<input> になりました。

ref を受け取らないコンポーネントに ref を渡すとどうなるかも確かめました。React 19.3.1 では、ref.current が null のままで、警告は何も出ませんでした。React 18 で MyInput に出たような「関数コンポーネントには ref を渡せない」という警告はありません。

一方、tsc はこの誤りを見つけます。ref を型に書いていないコンポーネントに渡した行(13行目)がエラーになりました。<div> 用の ref を、MyInput に渡した行(14行目)と、ComponentProps<'input'> で受け取るコンポーネントに渡した行(15行目)もエラーです。15行目のエラーからも、ComponentProps<'input'> の ref が <input> 用の型になっていることが分かります。次の出力は、14行目と15行目のエラーの続きの説明(3行ずつ)を省いています。

text
src/TypeError.tsx(13,19): error TS2322: Type '{ ref: RefObject<HTMLInputElement | null>; }' is not assignable to type 'IntrinsicAttributes & { placeholder?: string | undefined; }'.
  Property 'ref' does not exist on type 'IntrinsicAttributes & { placeholder?: string | undefined; }'.
src/TypeError.tsx(14,16): error TS2322: Type 'RefObject<HTMLDivElement | null>' is not assignable to type 'Ref<HTMLInputElement> | undefined'.
src/TypeError.tsx(15,22): error TS2322: Type 'RefObject<HTMLDivElement | null>' is not assignable to type 'Ref<HTMLInputElement> | undefined'.
注意

React 19.3.1 では、ref を使わないコンポーネントに ref を渡しても、実行時の警告は出ませんでした。ref.current が null のまま気づきにくいので、props の型に ref を書いて tsc に確かめさせると安心です。

クラスコンポーネントはそのまま、element.ref は非推奨に

クラスコンポーネントは、これまでどおりです。発表記事の注記のとおり、クラスに渡した ref はインスタンスを指すため、props には入りません。検証でも、React 19 と React 18 のどちらでも、親の ref.current はクラスのインスタンスで、this.props.ref は undefined でした。

もう1つ変わったのが、JSX の要素オブジェクトから ref を読む場合です。React 19 のアップグレードガイドによると、element.ref は非推奨になり、element.props.ref を使います。検証では、React 19 で element.props.ref に渡した ref が入っていました。element.ref も同じ ref を返しましたが、読んだときに次のエラーが記録されました。

text
console.error: Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.

React 18 では、element.props.ref には入らず、element.ref だけが ref を返しました。子の要素を調べて ref を取り出すようなコードは、React 19 では element.props.ref に直す必要があります。

forwardRef はいらなくなったのか

検証と公式ドキュメントから分かったことを、場合ごとに分けると次のとおりです。

場合 forwardRef
React 19 で新しく書く関数コンポーネント いらない。ref を props で受け取る
React 19 に上げた既存のコンポーネント そのまま動く。将来の非推奨に備えて書き換えてよい
React 18 でも使われるコンポーネント まだ必要。React 18 では props の ref は届かない
クラスコンポーネント もともと使わない。ref はインスタンスを指す

React 19 だけを対象にするなら、forwardRef は書かなくてよくなりました。書き換えるときは、props の型に ref を足し、tsc で渡し間違いがないかを確かめるのが、検証で分かった手堅い進め方です。

​Posted by 86