はじめに
React Hook Formを使って日本酒のレビューを編集するフォームを作ります!
フォームの初期値をそれぞれの入力項目に反映させる方法で悩んだので備忘録も兼ねて記事を書きました。
コードだけ見たい方は完成形へどうぞ!
実装するもの
日本酒のレビューを編集するフォームです。初期値から変更があった時だけ保存ボタンを押下できるものにします。
入力項目は
- 名前
- 生産地
- レベル
- おすすめ度
の4つです。
React Hook Formの準備
useFormの引数にdefaultValues
を追加します。
const {
register,
handleSubmit,
setValue,
control,
formState: { isDirty, isValid, errors },
} = useForm<InputValues>({
mode: 'onChange',
defaultValues,
})
defaultValues
を設定することでformState のisDirty
で「初期値から変更があったか」を検知することができます。
初期値から変更があった時に保存するボタンをクリックできる状態にしたいのでボタンのdisabled
に条件を設定します。
{/* 初期値から変更がありバリデーションエラーがない時にsubmitできる */}
<button type="submit" disabled={!isDirty || !isValid}>
保存する
</button>
順番が前後してしまいました。このフォームの入力項目の型定義をします。
type InputValues = {
name: string
level: string
producingArea: {
label: string
value: string
}
review: string
}
余談:おすすめ度(review
)の型はnumberじゃない?
今回はformState のisDirty
を利用して初期値から値が変更されたかをチェックしたかったのでstring型にしていますが本当はnumber型の方が適切だと思います。
ラジオボタンのvalue
がstring
型のため初期値から値が変更されたかをチェックする際に型変換が必要になり、formState のisDirty
をうまく利用できませんでした。良い解決法をご教授願います
各入力項目に初期値を反映させる
準備が終わったので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
orvalue
props on<select>
instead of settingselected
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を使って候補が表示される入力形式にします。
basic-usageのコードでテキストボックスの入力値をstateで管理していたので、defaultValue
を引数で渡して初期値を反映させました。
// AutoSuggest Component
const [value, setValue] = useState('')
useEffect(() => {
// propsで受け取ったdefaultValue
if (defaultValue) {
setValue(defaultValue.label)
}
}, [defaultValue])
完成形
4つの入力項目に無事初期値を反映させることができました
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