はじめに
今回は、Reactでフォームデータをバインドする方法についてまとめておきます。
Reactでフォームデータを扱うときのポイントは、
- コンポーネントからのイベントは、 React が提供する Synthetic Event が適用される
- フォームの値をコンポーネントの state として持ち、値の反映には state のリフトアップを使う
らしいです。
フォームデータを扱う(その1)
- サンプルコード
import { ChangeEvent, FC, useState } from 'react'; const divisionCode = { 1: '区分1', 2: '区分2', 3: '区分3' } as const; type FormData = { name: string; age?: number; division: keyof typeof divisionCode; }; const AbouutForm: FC = () => { const [formData, setFormData] = useState<FormData>({ name: '', age: undefined, division: 1, }); const changeHandler = ( event: ChangeEvent<HTMLInputElement | HTMLSelectElement>, ) => { const { name } = event.target; const value = event.target.value; setFormData((f) => ({ ...f, [name]: value })); }; const clickButtonHandler = () => { console.log(formData); }; return ( <div style={{ marginTop: '50px', textAlign: 'left', borderWidth: '2px', borderColor: '#ff0000', borderStyle: 'solid', }} > <div> <label>なまえ</label> <input name="name" type="text" value={formData.name} onChange={changeHandler} /> </div> <div> <label>年齢</label> <input type="number" name="age" value={formData.age} onChange={changeHandler} /> </div> <div> <label>区分</label> <select name="division" placeholder="区分を選択…" value={formData.division} onChange={changeHandler} > {Object.entries(divisionCode).map(([code, name]) => ( <option value={code} key={code}> {name} </option> ))} </select> </div> <div> <button onClick={clickButtonHandler}>テスト</button> </div> </div> ); }; export default AbouutForm;
解説
制御されたコンポーネント
- 画面のフォームデータは、formDataとしてstateで持ち回っています。
Synthetic Event について
- Reactは、単方向バインディングになっているので、
value={formData.name}
とすることで、初期値がセットされます。 - 単方向なので、画面上で入力された値をformDataのstateに反映させる必要があります。
- onClickやonChange属性のコールバック関数の引数のイベントが、React独自の
SyntheticEvent
オブジェクトになります。 - inputタグのonChange属性であれば、
SyntheticEvent
オブジェクトから拡張されたChangeEventを適用する必要がある。 - なので、onChagen属性で指定しているコールバック関数の引数の型は、
ChangeEvent<T>
となっています。- Tは、
HTMLInputElement
などのHTML要素の型を指定します。
- Tは、
- このonChange属性のコールバック関数で、stateを更新し、画面入力との同期を図ります。
- onClickやonChange属性のコールバック関数の引数のイベントが、React独自の
- 上記のサンプルコードでは、inputのonChangeで
changeHandler
関数を呼び出し、changeEventから入力された値を取得し、stateに反映しています。- なお、inputのname属性で、stateの値を更新するので、stateのプロパティ名とinputのname属性は揃えておく必要があります。
フォームデータを扱う(その2:フォームヘルパーを使用する)
フォームヘルパーには何がある?
Reactのフォームヘルパーは、formik
やreact-hook-form
などいろいろあるみたいですが、
npm trendsを見る限り、主流はreact-hook-form
のようです。
React Hook Formを使う
- インストール
npm i react-hook-form
React Hook Formを使った実装
- サンプルコード
import { FC } from 'react'; import { useForm } from 'react-hook-form'; const divisionCode = { 1: '区分1', 2: '区分2', 3: '区分3' } as const; type FormData = { name: string; age?: number; division: keyof typeof divisionCode; }; const AbouutForm2: FC = () => { const { register, reset, getValues } = useForm<FormData>({ defaultValues: { name: '', division: 1, }, }); const resetHandler = () => { console.log('reset'); reset(); }; const clickButtonHandler = () => { console.log('clickButtonHandler'); console.log(getValues()); }; return ( <div style={{ marginTop: '50px', textAlign: 'left', borderWidth: '2px', borderColor: '#ff0000', borderStyle: 'solid', }} > <div> <label>なまえ</label> <input {...register('name')} /> </div> <div> <label>年齢</label> <input type="number" {...register('age')} /> </div> <div> <label>区分</label> <select placeholder="区分を選択…" {...register('division')}> {Object.entries(divisionCode).map(([code, name]) => ( <option value={code} key={code}> {name} </option> ))} </select> </div> <div> <button onClick={clickButtonHandler}>テスト</button> <button onClick={resetHandler}>リセット</button> </div> </div> ); }; export default AbouutForm2;
解説
useForm
useForm<T>
のTでstateする型を指定するuseForm
の引数は、様々な指定ができるdefaultValues
: フォーム要素のデフォルト値を設定する- 詳細は、こちらを参照にするとよい。
useForm
の戻り値も、様々なものがオブジェクトとして含まれている。- 必要となるもののみ分割代入で使用するようにするとよい
- 詳細は、こちらを参照にするとよい。
register
: フォーム要素をReactHookFormの管理下に置くように登録するもの{...register('name')}
をログ出力すると、{name: 'name', onChange: ƒ, onBlur: ƒ, ref: ƒ}
のように出力されます。- つまり、フォーム要素で、
<input {...register('name')} />
のように使うことで、name属性、onChange属性、onBlur属性を設定してくれます。 - これは、フォームデータを扱う(その1)の実装に比べてだいぶ楽になります。
- つまり、フォーム要素で、
- 必要となるもののみ分割代入で使用するようにするとよい
getValues
useForm
で生成されたgetValues
関数を実行することで、フォームデータを取得できます。
reset
useForm
で生成されたreset
関数を実行することで、フォームデータをdefalutValuesに戻してくれます。
フォームデータを扱う(その3:バリデーション)
yupを使う
- インストール
npm i yup @hookform/resolvers
yupを使った実装
- サンプルコード
import { FC } from 'react'; import { useForm } from 'react-hook-form'; import * as yup from 'yup'; import type { InferType } from 'yup'; import { yupResolver } from '@hookform/resolvers/yup'; import FormErrorMessage from './FormErrorMessage'; import type { SubmitHandler } from 'react-hook-form'; const divisionCode = { 1: '区分1', 2: '区分2', 3: '区分3' } as const; type FormData = { name: string; age?: number; division: number; }; yup.setLocale({ mixed: { notType: '不正な値が設定されています', }, }); const regFormSchema = yup.object({ name: yup .string() .max(10, '10文字以下で入力してね🎵') .required('必須項目です'), age: yup.number().max(120, '120歳以下しか受け付けません!'), division: yup.number().required('必須項目です'), }); type RegFormSchema = InferType<typeof regFormSchema>; const AbouutForm2: FC = () => { const { register, handleSubmit, reset, formState: { errors }, } = useForm<FormData>({ defaultValues: { name: '', division: 1, }, resolver: yupResolver(regFormSchema), }); const resetHandler = () => { console.log('reset'); reset(); }; const onSubmit: SubmitHandler<RegFormSchema> = (data) => { console.log(data); }; return ( <div style={{ marginTop: '50px', textAlign: 'left', borderWidth: '2px', borderColor: '#ff0000', borderStyle: 'solid', }} > <form onSubmit={handleSubmit(onSubmit)}> <div> <label>なまえ</label> <input {...register('name')} /> <FormErrorMessage>{errors.name?.message}</FormErrorMessage> </div> <div> <label>年齢</label> <input type="number" {...register('age')} /> <FormErrorMessage>{errors.age?.message}</FormErrorMessage> </div> <div> <label>区分</label> <select placeholder="区分を選択…" {...register('division')}> {Object.entries(divisionCode).map(([code, name]) => ( <option value={code} key={code}> {name} </option> ))} </select> <FormErrorMessage>{errors.division?.message}</FormErrorMessage> </div> <div> <button type="submit">送信</button> <button onClick={resetHandler}>リセット</button> </div> </form> </div> ); }; export default AbouutForm2;
解説
yup.object
- yup.objectで入力フォームのバリデーションの設定を行います。
- なお、number型のチェックにおいて、ブランク時のチェックで
must be a
numbertype, but the final value was: NaN (cast from the value "")
みたいなエラーメッセージになるのでyup.setLocale
でメッセージを日本語化しておきます。- このあたりの共通化は課題ですね・・・。。。
- なお、number型のチェックにおいて、ブランク時のチェックで
yupResolver(regFormSchema)
- yup.objectでバリデーションの生成ができたら、useFormのresolverに渡します。
- また、バリデーションエラーの情報を使うために、useFormの
formState
からerrors
を使用しています。- このerrorsから画面にエラーメッセージを表示させています。
課題
- 上記のサンプルでは、「リセット」ボタンを押下したときに、エラーメッセージがクリアされないです。
- そこも含めてリセットしたいのですが、今のことろ策なしです…。