React-Hook-FormでRefを自作コンポーネントに渡したときのエラー・対処&改善

## はじめに

本業でNext.js（React）,TypeScriptを使用してて、React-Hook-Formを実装する際に**ref**を含む`register`を自作コンポーネントを渡した時に、
`Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?`
とエラーが出てのでその対処法についてのアウトプットです。

また、子コンポーネントにinputの属性含むpropsの型定義が冗長になってたのでrefを含む**ComponentPropsWithoutRef**を使って対処・リファクタリングも行いました。

:::note warn
**ref**についての説明は省いております。
:::

## 対処したコード

今回は自作したlabel付きのInputコンポーネントに対してバリデーションを実装したので、labelの型を含ませた子コンポーネントになっております。

```typescript

type LabelProps = {
  label: string;
}

type ChildProps = ComponentPropsWithoutRef<'input'> & LabelProps;

const ChildInput = React.forwardRef<HTMLInputElement, ChildProps>(
({ label, ...props }, ref) => {

  return (
    <div>
      <p>{label}</p>
      <input placeholder={label} {...props} ref={ref} />
    </div>
  );
});

ChildInput.displayName = "ChildInput"

```

## 該当エラー
### 親コンポーネント
```typescript
import React, { FC } from "react";
import {useForm, SubmitHandler} from "react-hook-from";
import ChildInput from "./ChildInut";

const Parent: FC = () => {

  const {
      handleSubmit,
      register,
      formState: { errors },
	  } = useForm<{parent: string}>({
	    defaultValues: { parent: ''},
	  });

  const onSubmit: SubmitHandler = (data) => {
      console.log(data)
   };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
  	  <ChildInput
        {...register('parent', { required: true })}
        label='子供にRefを投げる!!'
      />
	  <button>送信</button>
    </form>
  )
export default Parent
```

### 子コンポーネント
```typescript

import React, { FC } from "react";

type ChildProps = {
  name: string;
  type: string;
  value: string;
  onChange: (e: ChangeEvent<HTMLInputElement>) => void;
  label: string;
}


const ChildInput: FC<ChildProps> = ({ label, name, type, value, onChange }) => {

  return (
    <div>
      <p>{label}</p>
      <input
        name={name}
        type={type}
        value={value}
        onChange={onChange}
        placeholder={label}
      />
    </div>
  );
};

export default ChildInput;
```

以上の子コンポーネントに**ref**を含む`register`をそのまま渡すと**関数コンポーネントにrefを渡すことができない**とエラーが表示されます。
なので**React.fowardRefを使ってね**と言っているので自作のコンポーネントに対して**React.fowardRef**を使ってみましょう。

また、各HTML要素の属性とrefの型を含む**ComponentPropsWithoutRef**を使ってスッキリさせていきましょう。

## 解決

```diff_typescript

- import React, { FC } from "react";
+ import React, { forwardRef, ComponentPropsWithoutRef } from "react";

- type ChildProps = {
-   name: string;
-   type: string;
-   value: string;
-   onChange: (e: ChangeEvent<HTMLInputElement>) => void;
-   label: string;
- }

+ type LabelProps = {
+   label: string;
+ }

+ type ChildProps = ComponentPropsWithoutRef<'input'> & LabelProps;

- const ChildInput: FC<ChildProps> = ({ label, ...props }) => {
+ const ChildInput = forwardRef<HTMLInputElement, ChildProps>(
+ ({ label, ...props }, ref) => {

  return (
    <div>
      <p>{label}</p>
-       <input
-         name={name}
-         type={type}
-         value={value}
-         onChange={onChange}
-         placeholder={label}
-       />
+       <input placeholder={label} {...props} ref={ref} />
    </div>
  );
- }
+ });

+ ChildInput.displayName = "ChildInput"

export default ChildInput;
```

## 詳細

### ・ forwardRefとは

> ~ Reactのドキュメントから引用 ~
>コンポーネントが ref を受け取って、それをさらに下層の子に渡せる（つまり、ref を “転送” できる）ようになります。

ドキュメントにもあるように、自作したコンポーネントのDOM要素にrefを渡す時に使います。


### ・ ComponentPropsWithoutRefについて
ref要素を含むコンポーネントのpropsの型を作成することが出来る。
同様にComponentPropsWithRefでも使用できますが、違いとしては**ComponentPropsWithoutRef**と比べてクラスコンポーネントにも対応しているそうです。

以下はReactの型定義から参照したものです。
<details><summary>ComponentPropsWithoutRef/ComponentPropsWithRef 型の違い</summary><div>

```typescript

 type ComponentPropsWithRef<T extends ElementType> =
        T extends ComponentClass<infer P>
            ? PropsWithoutRef<P> & RefAttributes<InstanceType<T>>
            : PropsWithRef<ComponentProps<T>>;

 type ComponentPropsWithoutRef<T extends ElementType> =
       PropsWithoutRef<ComponentProps<T>>;

```
</div></details>

### ・ ESLint Error: Component definition is missing display name eslint(react/display-name)

```diff_typescript
+ ChildInput.displayName = "ChildInput"
```

ここの箇所については、ESLintにデバック時の関数名が無いとエラーが出たの追加しました。
`Component definition is missing display name eslint(react/display-name)`

おそらく**forwardRef**で返す関数が無名関数になっている影響ですね。

## まとめ
- **ref**をコンポーネントに渡す時は、**fowardRef**でコンポーネントをラップすることでコンポーネント内のDOMに**ref**を渡すことができる。

- TypeScriptを使用している場合は、**ComponentPropsWithRef**あるいは**ComponentPropsWithoutRef**で**ref**と該当のHTML属性を含んだpropsを子コンポーネントが受け取ることが出来る。

ComponentPropsWithoutRefでrefと各HTML要素にも対応してたので感動しました泣
おかげでコードがスッキリになりました✨

引き続きTypeScriptとReactについて学習していきます!!