かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

TypeScript React forwardRef の型をがんばる

react-hook-form というライブラリを使ってフォームを作っていたのですが、input や textarea が Atom レベルのコンポーネントになっており、ライブラリの都合で ref を渡す必要があったので forwardRef の型を頑張ったメモ。

"react": "17.0.2"
"typescript": "4.4.2"

forwardRef<refの対象, props> で書けば OK

type ButtonProps = JSX.IntrinsicElements['button'];
const ButtonComponent = forwardRef<HTMLButtonElement, ButtonProps>(
  (props, ref) => {
    return <button {...props} ref={ref} />
  }
);

ESLint で missing display name というエラーになる

ESLint の設定では次のようなエラーが表示されます。
Component definition is missing display name eslint(react/display-name)

DisplayName allows you to name your component. This name is used by React in debugging messages.
cf. eslint-plugin-react/display-name.md at master · yannickcr/eslint-plugin-react · GitHub

デバック時に関数名が無いと分かりづらいから関数名をつけることを強制するルールのようです。
つまり forwardRef で返す関数が無記名関数なアロー関数になっているので、ここに関数名を付けろということっぽい。

1. アロー関数をやめる

const ButtonComponent = forwardRef<HTMLButtonElement, ButtonProps>(
- (props, ref) => {
+ (function ButtonComponent(props, ref) {
    return <button {...props} ref={ref} />
  }
);

エラーにならないし、スコープが違うから問題ないのだけれど ButtonComponent という変数と function ButtonComponent があってちょっと気持ち悪いかも?

2. displayName プロパティを付けてしまう

const ButtonComponent = forwardRef<HTMLButtonElement, ButtonProps>(
  (props, ref) => {
    return <button {...props} ref={ref} />
  }
);
+ ButtonComponent.displayName = 'ButtonComponent';

これはこれで無理矢理感ある気もするけれど…

ESLint のドキュメントには上記2つの書き方が載っていたので、どちらでも良さそうです。 (個人的にどちらもあまりしっくりこない)

おまけ forwardRef の型定義を見てみる

forwardRef

interface ForwardRefExoticComponent<P> extends NamedExoticComponent<P> {
        defaultProps?: Partial<P> | undefined;
        propTypes?: WeakValidationMap<P> | undefined;
    }

function forwardRef<T, P = {}>(
  render: ForwardRefRenderFunction<T, P>
): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;

TP という型を受け取って ForwardRefRenderFunction<T, P> という型の render を引数にとって、ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> 型を返す。
forwardRef() 内に渡すコンポーネントの部分は ForwardRefRenderFunction<T, P>

ForwardRefRenderFunction

type ForwardedRef<T> = ((instance: T | null) => void) | MutableRefObject<T | null> | null;

interface ForwardRefRenderFunction<T, P = {}> {
        (props: PropsWithChildren<P>, ref: ForwardedRef<T>): ReactElement | null;
        displayName?: string | undefined;
        // explicit rejected with `never` required due to
        // https://github.com/microsoft/TypeScript/issues/36826
        /**
         * defaultProps are not supported on render functions
         */
        defaultProps?: never | undefined;
        /**
         * propTypes are not supported on render functions
         */
        propTypes?: never | undefined;
    }

forwardRef() にわたすコンポーネントの部分は (props: PropsWithChildren<P>, ref: ForwardedRef<T>): ReactElement | null => {...}displayName のプロパティがあるから function Foo() を渡しても OK ってことかな? (理解できているわけではない)
forwardRef<T, P> なのにコンポーネント部分で (P, T) => {} になってるの紛らわしくないですか???

所管

Chakra UI とか使ってたらよしなに定義されてるから気にしてなかったけど、汎用性の有るコンポーネント作るのめちゃめちゃ難しい…
本件と関係ないけど react-hook-form 色んなものが any 型に推論されるせいでめちゃめちゃ難しい。どうやって使うの??? TypeScript なんもわからんくなった…
TypeScript まだまだ出来ないので型問題でハマるとめちゃめちゃ疲れる。


[参考]