React.ComponentPropsを使ったコンポーネントの Props 設計

# はじめに
汎用的なElementレイヤーのコンポーネントを作るときの Props定義はこうした方がよいのではないか、という話です。

※ここで言うElementレイヤーとは： input, button, label など、atomic design でいう atom の部分

# ComponentPropsを使おう
このように一つ一つのプロパティを定義するより

```tsx:input.tsx
type Props = {
  value?: string;
  onChange?: React.ChangeEventHandler<HTMLInputElement>;
};
export const Input = ({ value, onChange }: Props) => (
  <input value={value} onChange={onChange} />
);
```

``ComponentProps``を利用して
下記のようにした方がよい、ということです。
（element が持つデフォルトの props を全て受け取れるように）

```tsx:input.tsx
// input element が持つ props を全て受け取れるようになる
type Props = React.ComponentProps<'input'>;
export const Input = (props: Props) => <input {...props} />;
```

``ComponentProps``を利用することで、React が提供している Element が持つ Props を全てうけとれるようになります。

- ``ComponentProps``
- ``ComponentPropsWithRef``：refを許容
- ``ComponentPropsWithoutRef``：refを許容しない

※個人的には``ref``も許容したい場面が多いので``ComponentPropsWithRef``を利用することが多いです。

# ComponentPropsを使うメリット
メリットは**コンポーネントのメンテナンスコストが軽減される**ことにあります

### ComponentPropsを使わない場合
作成当初は``value``, ``onChange``のみを受け取れるコンポーネントとして作成

```tsx:input.tsx
type Props = {
  value?: string;
  onChange?: React.ChangeEventHandler<HTMLInputElement>;
};
export const Input = ({ value, onChange }: Props) => (
  <input value={value} onChange={onChange} />
);
```
``type``や``onBlur``を利用する要件に対応させたくなった場合
``props``を追加する必要があり、都度コンポーネントに手を加える必要があります。

```tsx:input.tsx
type Props = {
  value?: string;
  onChange?: React.ChangeEventHandler<HTMLInputElement>;
  type?: string; // 追加
  onBlur?: React.FocusEventHandler<HTMLInputElement>; // 追加
};
export const Input = ({ value, type, onChange, onBlur, onKeyPress }: Props) => (
  <input
    value={value}
    onChange={onChange}
    type={type} // 追加
    onBlur={onBlur} // 追加
  />
);
```

### ComponentPropsを使うと
``type``や``onBlur``を利用する要件に対応させたくなった場合も
その都度``props``を追加する必要がなくなります。

```tsx:input.tsx
/**
 * React.ComponentProps<'input'>; で
 * input要素が持つPropsを全て受け取れるようになる
 *
 * value?: string;
 * type?: string;
 * onChange?: React.ChangeEventHandler<HTMLInputElement>;
 * onBlur?: React.FocusEventHandler<HTMLInputElement>;
 * onFocus?: React.FocusEventHandler<HTMLInputElement>;
 * onKeyPress?: React.KeyboardEventHandler<HTMLInputElement>;
 * etc...
 */
type InputProps = React.ComponentProps<'input'>;
export const Input = (props: InputProps) => {
  // 分割代入で全ての値を渡すことができる
  return <input {...props} />;
};
```
``props``を全てそのまま渡すのではなく、特定の ``props`` ではコンポーネント独自の振る舞いをさせたいときは、``{...props}`` の下にその``props``を渡します。

ex)``onChange`` では特定の振る舞いを強制させたいとき

```tsx:input.tsx
type Props = React.ComponentProps<'input'>;
export const Input = (props: Props) => {
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    // このコンポーネントで onChange 発火時に必ず実行したい振る舞いを書く
    props.onChange?.(e);
  };
  return (
    <input
      {...props} // <- onChange が含まれるものの
      onChange={handleChange} // <- こちらの handleChange が優先される
    />
  );
};
```

# より実践的な使い方
より実践的な使い方として下記のようなインプットコンポーネントを例に考えてみます。

**デフォルト**
![スクリーンショット 2023-02-12 13.34.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621162/3c23bbf6-e6ab-8e7c-be73-a0b513b974c8.png)

**エラー**
![スクリーンショット 2023-02-12 13.35.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621162/287b5260-ac3b-fbbc-e8f2-143a3631c808.png)

先ほどとは異なり、コンポーネント独自のPropsをもたせる必要がでてくるかと思います。
```tsx:input.tsx
type InputElementProps = React.ComponentProps<'input'>;
type Props = {
  title: string; // コンポーネント独自のprops
  errorMessage?: string; // コンポーネント独自のprops
  inputElementProps?: InputElementProps; // inputElementのprops
};
export const InputComponent = ({
  title,
  errorMessage,
  inputElementProps,
}: Props) => {
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    // このコンポーネントで onChange 発火時に必ず実行したい振る舞いを書く
    inputElementProps?.onChange?.(e);
  };
  return (
    <div style={rootStyle}>
      <p style={titleStyle}>{title}</p>
      <input {...inputElementProps} onChange={handleChange} />
      <p style={errorStyle}>{errorMessage}</p>
    </div>
  );
};
```

**コンポーネントを利用するとき**
```tsx:index.tsx
const Index = () => {
  const [value, setValue] = useState('');
  return (
    <div>
      <InputComponent
        title="タイトル"
        errorMessage="エラーメッセージ"
        inputElementProps={{
          value,
          onChange: (e) => setValue(e.target.value),
        }}
      />
    </div>
  );
};
```

### Element がもつ props の仕様を変更したい場合
さらに、``input`` の ``value type`` を ``password`` と ``email`` だけに限定させたい、というユースケースがあったとします。

その場合、``input``が持つデフォルトの``props``である``type``をコンポーネントで独自に型定義して、``InputElementProps``は``type``を除いた型にします。

```tsx:input.tsx
type InputElementProps = Omit<React.ComponentProps<'input'>, 'type'>; // input element の props から type が除外される
type Props = {
  title: string;
  type: 'password' | 'email'; // password と email に限定（inputのデフォルトでは string型）
  errorMessage?: string;
  inputElementProps?: InputElementProps; // inputElementのprops
};
export const InputComponent = ({
  title,
  type,
  errorMessage,
  inputElementProps,
}: Props) => {
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    // このコンポーネントで onChange 発火時に必ず実行したい振る舞いを書く
    inputElementProps?.onChange?.(e);
  };
  return (
    <div style={rootStyle}>
      <p style={titleStyle}>{title}</p>
      <input 
        {...inputElementProps}
        type={type} // ここで渡すのを忘れずに
        onChange={handleChange}
      />
      <p style={errorStyle}>{errorMessage}</p>
    </div>
  );
};
```

**コンポーネントを利用するとき**
```tsx:index.tsx
const Index = () => {
  const [value, setValue] = useState('');
  return (
    <div>
      <InputComponent
        title="タイトル"
        errorMessage="エラーメッセージ"
        type='password' // password or email しか渡せない
        inputElementProps={{
          value,
          onChange: (e) => setValue(e.target.value),
          type: 'number' // error: 除外しているのでここでは渡せない
        }}
      />
    </div>
  );
};
```

# 最後に
自分自身もコンポーネント設計は、現状迷いながら実装している部分が多いです！
こうした方がよいのでは？などあれば気軽にコメントお待ちしております！

最後までお読みいただきありがとうございました！

### 参考資料
ありがとうございました。
- smarthr-ui：https://github.com/kufu/smarthr-ui
- mui：https://github.com/mui/material-ui
- Takepepeさん：https://zenn.dev/takepepe/articles/atoms-type-definitions