React Hook Formで初期値のあるフォームを実装する

## はじめに

[React Hook Form](https://react-hook-form.com/)を使って日本酒のレビューを編集するフォームを作ります！
フォームの初期値をそれぞれの入力項目に反映させる方法で悩んだので備忘録も兼ねて記事を書きました。

コードだけ見たい方は[完成形](#完成形)へどうぞ！

## 実装するもの

日本酒のレビューを編集するフォームです。初期値から変更があった時だけ保存ボタンを押下できるものにします。
![スクリーンショット 2022-12-01 18.55.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982085/ee75d8f1-a272-15df-2259-9bcb09841365.png)

入力項目は

- 名前
- 生産地
- レベル
- おすすめ度
の4つです。

## React Hook Formの準備

[useForm](https://react-hook-form.com/api/useform#main)の引数に`defaultValues`を追加します。

```react
const {
    register,
    handleSubmit,
    setValue,
    control,
    formState: { isDirty, isValid, errors },
  } = useForm<InputValues>({
    mode: 'onChange', 
    defaultValues,
  })
```

`defaultValues`を設定することで[formState](https://react-hook-form.com/api/useform/formstate#main) の`isDirty` で「初期値から変更があったか」を検知することができます。

初期値から変更があった時に保存するボタンをクリックできる状態にしたいのでボタンの`disabled`に条件を設定します。

```react
{/* 初期値から変更がありバリデーションエラーがない時にsubmitできる */}
<button type="submit" disabled={!isDirty || !isValid}>
  保存する
</button>
```

順番が前後してしまいました。このフォームの入力項目の型定義をします。

```react
type InputValues = {
  name: string
  level: string
  producingArea: {
    label: string
    value: string
  }
  review: string
}
```

余談：おすすめ度（`review`）の型はnumberじゃない？
今回は[formState](https://react-hook-form.com/api/useform/formstate#main) の`isDirty`を利用して初期値から値が変更されたかをチェックしたかったのでstring型にしていますが本当はnumber型の方が適切だと思います。

ラジオボタンの`value`が`string`型のため初期値から値が変更されたかをチェックする際に型変換が必要になり、[formState](https://react-hook-form.com/api/useform/formstate#main) の`isDirty`をうまく利用できませんでした。良い解決法をご教授願います:bow:

## 各入力項目に初期値を反映させる

準備が終わったので[useForm](https://react-hook-form.com/api/useform#main)に渡した`defaultValues`をそれぞれの入力項目に反映させます。

### テキスト（input type="text"）

テキスト入力の場合は[Example](https://react-hook-form.com/get-started) に書いてある通りに`defaultValue`に文字列を渡すだけで完了です。

```react
<input defaultValue="編集前の値" {...register("name")} />
```

### ラジオボタン（input type="radio"）

初めに`checked`で値の比較を行っていました。
初期値との比較を行うのに[useWatch](https://react-hook-form.com/api/usewatch/#main)を使うのはやりすぎと思っていたら`defaultChecked`を使うのですね。勉強になりました。

#### 間違い

```react
const reviewField = useWatch({ control, name: "review" })
return (
  <input              
    type="radio"
    {...register('review', { required: true })}
    value={item.value}
    {/* 初期値だけでなく常に入力値を監視している状態になる。 */}
    checked={item.value === reviewField}
  />
)
```

#### 正しい

`checked`ではなく`defaultChecked`で初期値を設定する

```react
<input
  className="radioGroup__radio"
  type="radio"
  {...register('review', { required: true })}
  value={item.value}
  defaultChecked={item.value === defaultValues?.review}
/>
```

チェックボックスの場合も同様に`defaultChecked`を使いましょう。

### セレクトボックス（Select）

HTMLと同じで`selected`を設定すれば良いと思っていました。Reactに怒られてしまうので`defaultValue`を使います。

#### 間違い

```react
<select name={name} id={id} ref={ref} {...rest}>
  <option value="" disabled>
    選択してください
  </option>
  {options.map(option => (
    <option value={option.value} key={option.value} selected={defaultValue === option.value}>
      {option.label}
    </option>
  ))}
</select>
```

Reactに怒られました
> Warning: Use the `defaultValue` or `value` props on `<select>` instead of setting `selected` on `<option>`.
>

#### 正しい

```react
<select name={name} id={id} ref={ref} {...rest} defaultValue={defaultValue}>
  <option value="" disabled>
    選択してください
  </option>
  {options.map(option => (
    <option value={option.value} key={option.value}>
      {option.label}
    </option>
  ))}
</select>
```

### 候補が表示されるテキスト入力（AutoSuggest)

生産地も普通のセレクトボックスで良いですが使ってみたかった[react-autosuggest](https://github.com/moroshko/react-autosuggest)を使って候補が表示される入力形式にします。
![183d5a763d768a4c5b048e6a9ecf22a3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982085/60773d3f-e1d1-f605-1dfd-2a4c6920535d.gif)

[basic-usage](https://github.com/moroshko/react-autosuggest#basic-usage)のコードでテキストボックスの入力値をstateで管理していたので、`defaultValue`を引数で渡して初期値を反映させました。

```react
// AutoSuggest Component
const [value, setValue] = useState('')

useEffect(() => {
  // propsで受け取ったdefaultValue
  if (defaultValue) {
    setValue(defaultValue.label)
  }
}, [defaultValue])
```

## 完成形

4つの入力項目に無事初期値を反映させることができました:clap:
importしているコンポーネントなどは[Github](https://github.com/2n-75/form-has-default-value-demo)をご確認ください！

```react
import React from 'react'
import { useForm } from 'react-hook-form'

import { AutoSuggest } from './components/AutoSuggest'
import { InputText } from './components/InputText'
import { SelectBox } from './components/SelectBox'
import { levelOptions, producingAreaOptions, reviewOptions } from './const'

type InputValues = {
  name: string
  level: string
  producingArea: {
    label: string
    value: string
  }
  review: string
}

const App = () => {
  const defaultValues = {
    name: '小松原',
    level: 'beginner',
    producingArea: {
      label: '新潟県',
      value: '新潟県',
    },
    review: '5',
  }
  const {
    register,
    handleSubmit,
    setValue,
    formState: { isDirty, isValid },
  } = useForm<InputValues>({
    mode: 'onChange',
    defaultValues,
  })
  const onSubmit = (values: InputValues) => {
    console.log({ values })
  }
  return (
    <div className="container">
      <form onSubmit={handleSubmit(onSubmit)} className="form">
        <div className="form__item">
          <label htmlFor="name" className="form__label">
            名前
          </label>
          <InputText {...register('name', { required: '必須項目です' })} placeholder="なまえ" id="name" />
        </div>
        <div className="form__item">
          <label htmlFor="producingArea" className="form__label">
            生産地
          </label>
          <AutoSuggest
            id="producingArea"
            options={producingAreaOptions}
            placeholder="東京"
            defaultValue={defaultValues?.producingArea}
            onSelectSuggestion={option => setValue('producingArea', option)}
          />
        </div>
        <div className="form__item">
          <label htmlFor="level" className="form__label">
            レベル
          </label>
          <SelectBox
            {...register('level', { required: true })}
            id="level"
            name="level"
            options={levelOptions}
            defaultValue={defaultValues?.level}
          />
        </div>
        <div className="form__item">
          <label htmlFor="review" className="form__label">
            おすすめ度
          </label>
          <div className="radioGroup">
            {reviewOptions.map(item => (
              <label className="radioGroup__label" key={item.value}>
                <input
                  className="radioGroup__radio"
                  type="radio"
                  {...register('review', { required: true })}
                  value={item.value}
                  defaultChecked={item.value === defaultValues?.review}
                />
                {item.label}
              </label>
            ))}
          </div>
        </div>
        <div className="form__footer">
          <button className="form__submitButton" type="submit" disabled={!isDirty || !isValid}>
            保存する
          </button>
        </div>
      </form>
    </div>
  )
}

export default App

```

### 参考

- [React Hook Form](https://react-hook-form.com/)
- [React Autosuggest](https://github.com/moroshko/react-autosuggest)