30
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

株式会社ゆめみAdvent Calendar 2024

Day 1

React19はforwardRef絡みの長ったらしい型記述を無くしてくれる

Last updated at Posted at 2024-11-30

2枚のカードが並ぶUI。それぞれのカードは薄い緑色で、そのうち1枚にはマウスホバーされていて、より淡い色に変化している。

TL;DR (3行まとめ)

  • React 18 までは、TypeScript で forwardRef するために、ref の型を書いたり、引数の順番・型引数の順番を気にしたり、めんどくさかった
  • React 19 では、forwardRef が不要になり、普通に ref Prop を受け取れる
  • TypeScript 型の記述量がかなり減って、書き間違いやすい部分も削減された

React18までの書き方

React 18 以前では、ref Prop を指定できるコンポーネントを書くために、forwardRef を使う必要がありました。

  • 生の button と同じように、属性、ref を設定できるカスタマイズ性を保つ
  • カスタマイズ性を保ったまま、CSS Modules でスタイルを設定する

という要件を満たす「button をラップしたカードUIコンポーネント」を作成するためには、以下のようなコードを書いていました。

(ちなみに、いわゆる Atomic Design の Atom 相当のコンポーネントをはじめ、汎用的なコンポーネントの多くは、以上の要件をキチンと満たすように実装する必要があります。その理由については、以下の記事を参考にしてください。)

card-old.tsx
import {
  type ComponentPropsWithoutRef,
  type ComponentRef,
  forwardRef,
} from "react";
import clsx from "clsx";

import styles from "./card.module.scss";

type RefType = ComponentRef<"button">;
type Props = ComponentPropsWithoutRef<"button">;

export const CardOld = forwardRef<RefType, Props>(function CardOld(
  { className, ...props },
  ref
) {
  return (
    <button ref={ref} {...props} className={clsx(className, styles.root)} />
  );
});
card.module.scss の中身はこちら
card.module.scss
.root {
  display: grid;
  padding: 16px;

  appearance: none;
  background-color: oklch(from green .93 .06 h);
  border: 1px solid oklch(from green .7 .05 h);
  color: oklch(from green .4 .05 h);
  border-radius: 4px;
  font-size: 1rem;

  cursor: pointer;

  &:hover {
    background-color: oklch(from green .98 .04 h);
  }
}

登場人物を下から順に見ていきましょう

forwardRef

forwardRef<T, P>((props: P, ref: ForwardedRef<T>) => ...) は、ref を受け取るコンポーネントを作るために(React v18 まで)必要だった関数です。

型引数 T は、「ref の中身」のようなものです。button タグをラップするコンポーネントを作るなら、 HTMLButtonElement明示的に 指定する必要があります。

型引数 P は、言わずと知れた Props の型として、 明示的に 指定する必要があります。

理由はわかりませんが、型引数は「Ref 関係の型 → Props の型」の順なのに、関数の引数は「propsref」の順になっています。この順番は忘れやすいですし、順番を取り違えてもあんまりピンとこないエラーが発生するので、ミスしやすいです。

ComponentPropsWithoutRef

ComponentPropsWithoutRef<Comp> ユーティリティ型 は、コンポーネント(またはHTMLタグ名の文字列)を引数として受け取って、「そのコンポーネントが受け取る Props の型」を表します。

名前の通り、ref を除いた Props の型 を取得できます。

ComponentRef

ComponentRef<Comp> ユーティリティ型 は、コンポーネント(またはHTMLタグ名の文字列)を引数として受け取って、「そのコンポーネントの ref に渡せる Ref の中身」を表します。

ややこしいので、具体的を挙げてお茶を濁します。

  • 文字列を指定した場合、HTML 標準要素に対応する型
    • ComponentRef<"button"> の結果は HTMLButtonElement
    • <button ref={ } ← に渡せる Ref の型は LegacyRef<HTMLButtonElement> なので
  • コンポーネントそのものの型を指定した場合、そのコンポーネントの受け取れる ref の型
    • ComponentRef<typeof CardOld> の結果は、HTMLButtonElement

React19ではforwardRefが不要になって記述が短くなる

React 19 では forwardRef なしで同じことができるようになります。

コンポーネントが ref Prop を受け取るように書く、ただそれだけで OK です!

card-new.tsx
import type { ComponentPropsWithRef, FC } from "react";
import clsx from "clsx";

import styles from "./card.module.scss";

type Props = ComponentPropsWithRef<"button">;

export const CardNew: FC<Props> = ({ className, ...props }) => {
  return <button {...props} className={clsx(className, styles.root)} />;
};

しかも、Ref および Props の型を "引用" する方法も少し変わりました。型関連のコードの繰り返しが減ってシンプルになったことが一目瞭然だと思います。

特別な登場人物は1人だけです。

ComponentPropsWithRef

ComponentPropsWithRef<Comp> ユーティリティ型 は、ComponentPropsWithoutRef<Comp> の仲間ですが、こちらは ref も含めた Props を取得できます。

React19(RC)の型定義のインストール方法

React 19 安定版が、2024/12/05 でリリースされました!なので、以下は気にせず型定義を利用できます!

React 19 は、2024/12/01 現在まだ RC なので、@types/react が使えず、少し特殊な記述が必要です。

{
  "dependencies": {
    "@types/react": "npm:types-react@rc",
    "@types/react-dom": "npm:types-react-dom@rc"
  },
  "overrides": {
    "@types/react": "npm:types-react@rc",
    "@types/react-dom": "npm:types-react-dom@rc"
  }
}

とりあえず create-next-app の v15 を使ってしまえば、コマンド一発でそこらへんの記述を出力してくれるので楽ちんです。 ついでに App Router も楽しんじゃおう!

30
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?