ReactHooksで複数の汎用フォーム部品を1つのstateとハンドラで管理するをTypeScriptで

フォーム部品のstateを一つのstateとハンドラで管理するにあたり、TypeScriptでなるべく型定義してみました。
なお、各HTML要素のインターフェース名はこちらで確認できます。
[Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API)

## フォーム部品のインターフェース

各フォーム部品が共通して実装するインターフェースの定義。
`name`と`value`、`onChange`が肝で、後はまあ共通して必要そうなものをいくつか。
それぞれのフォーム部品が扱うデータ型をジェネリック(Generics)で指定します。
テキストボックスはstring、チェックボックスはbooleanなど。

```ts
interface FormPartsProps<T> {
  name: string;
  value: T;
  onChange(param: { [name: string]: T }): void;
  label?: string;
  required?: boolean;
  errMsg?: string;
  onError?(param: { [name: string]: string }): void;
}
```

## State管理用Hook

フォーム部品をまとめて管理する用のHooksです。
各フォーム部品から`{ name: value }`の形式でデータを受け取り、現在のstateにマージさせます。

```tsx
type HandleChange = (params: { [name: string]: any }) => void;

const useFormState = <T = any>(initialState: T): [T, HandleChange] => {
  const [state, setState] = useState<T>(initialState);

  const handleChange = useCallback<HandleChange>(params => {
    setState(state => ({
      ...state,
      ...params,
    }));
  }, []);

  return [state, handleChange];
};
```

## 共通のインターフェースを持つフォーム部品たち

上記のインターフェース`FormPartsProps`の実装例。
部品独自に受け取りたいpropsがある場合は、継承して再定義します。
`onChange`関数へは、共通して`{ [name]: [anyValue] }`の形式でデータを渡します。
メール形式やパスワード強度など、部品独自のバリデート処理は部品側でやります。

```tsx
// 性別ラジオボタン
const SelectSex: React.FC<FormPartsProps<string>> = ({
  name,
  value,
  onChange,
}) => {
  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) =>
      onChange({ [name]: e.target.value }),
    [name, onChange],
  );

  return (
    <div>
      <label>
        <input type="radio"
          name={name}
          value="1"
          checked={value === '1'}
          onChange={handleChange}
        />
        男性
      </label>
      <label>
        <input type="radio"
          name={name}
          value="2"
          checked={value === '2'}
          onChange={handleChange}
        />
        女性
      </label>
      <label>
        <input type="radio"
          name={name}
          value="3"
          checked={value === '3'}
          onChange={handleChange}
        />
        ゴリラ
      </label>
    </div>
  );
};

// メール欄
const InputMail: React.FC<FormPartsProps<string>> = ({
  name,
  value,
  onChange,
  errMsg,
  onError,
}) => {
  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      if (validate(e.target.value)){
        onChange({ [name]: e.target.value });
      } else {
        onError({ [name]: 'AnyErrorMessage' });
      }
    },
    [name, onChange],
  );

  return (
    <div>
      <label>メール:</label>
      <input type="email" name={name} value={value} onChange={handleChange} />
      {errMsg ? <div>{errMsg}</div> : null}
    </div>
  );
};

// テキストエリア
interface TextAreaProps extends FormPartsProps<string> {
  placeholder?: string;
  maxlength?: number;
}

const TextArea: React.FC<TextAreaProps> = ({
  name,
  value,
  onChange,
  maxlength = 50,
  label = '備考',
}) => {
  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLTextAreaElement>) => {
      onChange({ [name]: e.target.value })
    },
    [name, onChange],
  );

  return (
    <div>
      <label>{label}:</label>
      <textarea
        name={name}
        value={value}
        onChange={handleChange}
        maxlength={maxlength}
      ></textarea>
    </div>
  );
};
```

## フォーム部品の設置

フォーム部品の設置例。
入力値を管理するstateとエラーを管理するstateをそれぞれ用意してます。
未入力チェックなどはこちらで確認します。

```tsx
const Form: React.FC<{ onSubmit: Function }> = ({ onSubmit }) => {
  const [data, setData] = useFormState<DataType>(initialData);
  const [error, setError] = useFormState<ErrorType>(initialError);
  const isButtonDisabled = Object.values(error).some(v => v);

  const handleSubmit = useCallback(
    (e: React.MouseEvent<HTMLFormElement>) => {
      e.preventDefault();
      if (validate(data)){
        onSubmit(data);
      } else {
        setError({[TargetName]: 'AnyErrorMessage'})
      }
    },
    [onSubmit, data],
  );

  return (
    <form onSubmit={handleSubmit}>
      <TextBox
        required
        name="last-name"
        label="姓"
        value={data.lastName}
        onChange={setState}
        errMsg={error.lastName}
        onError={setError}
      />
      <TextBox
        required
        name="first-name"
        label="名"
        value={data.firstName}
        onChange={setState}
        errMsg={error.firstName}
        onError={setError}
      />
      <SelectSex
        name="sex"
        value={data.sex}
        onChange={setState}
      />
      <InputMail 
        name="mail"
        value={data.mail}
        onChange={setState}
        errMsg={error.mail}
        onError={setError}
      />
      <TextArea
        name="description"
        label="その他"
        value={data.description}
        onChange={setState}
      />
      <button type="submit" disabled={isButtonDisabled}>
        送信
      </button>
    </form>
  );
};
```

Reactのフォームの実装は、こちらの記事にあるような元々のFormの機能を活用するのが一番シンプルで良いよなーとは思います。
[ReactでできるだけシンプルにForm実装](https://qiita.com/goodtea0223/items/4014e7e6effa68b17c62)
今回は、WebAPIからサジェストリストを取得するInputコンポーネントなど、実装がややこしい部品のややこしい部分を隠蔽しつつ汎用化させて使いまわしたいなと思い、作ってみた次第です。