28
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

NIJIBOXAdvent Calendar 2022

Day 7

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

Last updated at Posted at 2022-12-06

はじめに

React Hook Formを使って日本酒のレビューを編集するフォームを作ります!
フォームの初期値をそれぞれの入力項目に反映させる方法で悩んだので備忘録も兼ねて記事を書きました。

コードだけ見たい方は完成形へどうぞ!

実装するもの

日本酒のレビューを編集するフォームです。初期値から変更があった時だけ保存ボタンを押下できるものにします。
スクリーンショット 2022-12-01 18.55.42.png

入力項目は

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

React Hook Formの準備

useFormの引数にdefaultValuesを追加します。

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

defaultValuesを設定することでformStateisDirty で「初期値から変更があったか」を検知することができます。

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

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

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

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

余談:おすすめ度(review)の型はnumberじゃない?
今回はformStateisDirtyを利用して初期値から値が変更されたかをチェックしたかったのでstring型にしていますが本当はnumber型の方が適切だと思います。

ラジオボタンのvaluestring型のため初期値から値が変更されたかをチェックする際に型変換が必要になり、formStateisDirtyをうまく利用できませんでした。良い解決法をご教授願います:bow:

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

準備が終わったのでuseFormに渡したdefaultValuesをそれぞれの入力項目に反映させます。

テキスト(input type="text")

テキスト入力の場合はExample に書いてある通りにdefaultValueに文字列を渡すだけで完了です。

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

ラジオボタン(input type="radio")

初めにcheckedで値の比較を行っていました。
初期値との比較を行うのにuseWatchを使うのはやりすぎと思っていたらdefaultCheckedを使うのですね。勉強になりました。

間違い

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

正しい

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

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

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

セレクトボックス(Select)

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

間違い

<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>.

正しい

<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を使って候補が表示される入力形式にします。
183d5a763d768a4c5b048e6a9ecf22a3.gif

basic-usageのコードでテキストボックスの入力値をstateで管理していたので、defaultValueを引数で渡して初期値を反映させました。

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

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

完成形

4つの入力項目に無事初期値を反映させることができました:clap:
importしているコンポーネントなどはGithubをご確認ください!

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

参考

28
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?