Reactでのフォームの取り扱い
Reactでのフォームの取り扱いについて、要点をまとめました。
フォームデータの取り扱い
Reactでのフォームデータの取扱いは、大きく分けて2パターンあリます。
それぞれの違いを見ていきましょう。
「制御コンポーネント」 を用いる方法
フォームの値を親コンポーネントでstate
として管理し、値の反映には stateのリフトアップ を用いる。
stateのリフトアップが理解できていない方はこちらの記事をご確認ください。
- メリット
- 常に値にアクセスできるので、ユーザーの入力中にバリデーションが可能
- デメリット
- 入力値が更新されるたびにコンポーネントが再レンダリングされる
Reactは、親から子の単方向データフロー を採用しているため、基本的には制御コンポーネントを用いる方法が推奨されています。
「非制御コンポーネント」 を用いる方法
仮想DOMを経由せず直接DOM要素の値にアクセスする手法
useRef
を使用する
- メリット
- 入力値が更新されるたびにコンポーネントが再レンダリングされないので、パフォーマンス効率が良い
- デメリット
- ユーザーの入力中にバリデーションをすることが難しい
イベントパラメータについて
Reactでは、コンポーネントでイベントを扱う際、Event
オブジェクトではなく、React独自の Synthetic Event
を用います。
イベントパラメータを引数に受け取る場合は、SyntheticEvent
を継承した、特定のイベント型を指定する必要があります。
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, inputValue: event.target.value });
};
// ...
<input type="text" onChange={handleChange} />
イベントハンドラと対応するデータ型
type Props = {
onClick: (event: React.MouseEvent<HTMLInputElement>) => void
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
onkeypress: (event: React.KeyboardEvent<HTMLInputElement>) => void
onBlur: (event: React.FocusEvent<HTMLInputElement>) => void
onFocus: (event: React.FocusEvent<HTMLInputElement>) => void
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
onCancel: (event: React.React.SyntheticEvent<HTMLDialogElement>) => void
}
[サンプルコード] 制御コンポーネント
ここまで基本の理解はできたかと思います。
制御コンポーネントのサンプルコードを見ていきましょう。
前提
- フォーム全体のデータを親コンポーネントで
state
管理 - フォームデータを更新するためのコールバック関数を定義し、子コンポーネントのイベントとして設定
import React, { useState } from 'react';
type FormData = {
firstName: string;
lastName: string;
};
const ControlledComponent: React.FC = () => {
const [formData, setFormData] = useState<FormData>({
firstName: '',
lastName: '',
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prevData) => ({
...prevData,
[name]: value,
}));
};
return (
<form>
<input
type="text"
name="firstName"
value={formData.firstName}
onChange={handleChange}
/>
<input
type="text"
name="lastName"
value={formData.lastName}
onChange={handleChange}
/>
</form>
);
};
[サンプルコード] 非制御コンポーネント
-
useRef
を使用して、コンポーネントの参照を保持(仮想DOMを介さない) - 値の取得 / 更新時は
DOM
要素を直接参照 / 更新する
import React, { useRef } from 'react';
const UncontrolledComponent: React.FC = () => {
const firstNameRef = useRef<HTMLInputElement>(null);
const lastNameRef = useRef<HTMLInputElement>(null);
return (
<form>
<input type="text" name="firstName" ref={firstNameRef} />
<input type="text" name="lastName" ref={lastNameRef} />
</form>
);
};
[ 応用] React Hook Form
Reactにはフォームデータを扱うためのライブラリがいくつか用意されています。
デファクト・スタンダードとされているものはなく、代表的なものは以下の通りです。
- React Hook Form
- Formik
- React Final Form
npm trends
はこのような感じ(20240810時点)
React Hook Formとは?
フォームヘルパーの1つ。
複雑なフォームを扱う場合などに用いられる。
特徴として、非制御コンポーネントを用いて、フォームの入力データを管理します。
サンプルコード
React Hook Formを利用するまでの基本的な流れです。
1.ㅤuseForm
をインポート
2.ㅤuseForm
の引数にオプションを指定、戻り値で利用する機能をオブジェクトプロパティとして受け取る
const { register, handleSubmit, reset } = useForm<FormData>({
defaultValues: { username: '', isAgreed: false, },
});
3.ㅤregister
関数でフォーム要素を登録する
<input {...register('username')} />
- 引数で指定した値(今回の場合は
username
)がセットされた、name
属性が付与 - 戻り値に
ref
プロパティが含まれるため、非制御コンポーネントによるDOM
管理が実施
4.ㅤhandleSubmit
でのサブミット処理のハンドリング
const onSubmit: SubmitHandler<UserForm> = (data) => {
console.log(data);
};
//...
<form action="#" onSubmit={handleSubmit(onSubmit)}>
バリデーション
react-hook-form
にはバリデーションをサポートするカスタムリゾルバが複数ある。
カスタムリゾルバとは?
フォームバリデーションをカスタマイズするために使用。
独自のバリデーションロジックを実装したい場合に用いられる。
yup
カスタムリゾルバの1つ
旬が過ぎたかもしれないが...yup
の記事が多かったのでyup
を使用します。
サンプルコード
yup
を利用するまでの基本的な流れです。
1.ㅤsrc/schema
にフォームデータのスキーマを定義したファイルの作成
src/schema/userForm.ts
import * as yup from 'yup';
import type { InferType } from 'yup';
import { sex, userRole } from '@/constants/user_form_constants';
export const userForm = yup.object({
name: yup.string().required('名前の入力は必須です'),
email: yup.string().required('メールアドレスの入力は必須です'),
sex: yup.mixed().oneOf(Object.keys(sex)),
role: yup.mixed().oneOf(Object.keys(userRole)),
agreement: yup.boolean().oneOf([true], '利用規約の同意が必要です').required(),
});
export type UserFormSchema = InferType<typeof userForm>;
2.ㅤForm
コンポーネントにインポート
import { yupResolver } from '@hookform/resolvers/yup';
import type { UserFormSchema } from '@/schema/userForm';
import { userForm } from '@/schema/userForm';
3.ㅤuseForm
の変更
- ジェネリックにインポートしたスキーマのデータ型を指定
- 第二引数に
yupResolver
を指定 - 戻り値から
formState: { errors }
の取得
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<UserFormSchema>({
defaultValues: {
name: '',
email: '',
sex: 'man',
role: 'readonly',
agreement: false,
},
resolver: yupResolver(userForm),
});
4.ㅤエラーメッセージの表示
{errors.username?.message}
最終的なコード
import { yupResolver } from '@hookform/resolvers/yup';
import { SubmitHandler, useForm } from 'react-hook-form';
import type { UserFormSchema } from '@/schema/userForm';
import { sex, userRole } from '@/constants/user_form_constants';
import { userForm } from '@/schema/userForm';
const SampleReactHookYupResolver = () => {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<UserFormSchema>({
defaultValues: {
name: '',
email: '',
sex: 'man',
role: 'readonly',
agreement: false,
},
resolver: yupResolver(userForm),
});
const onReset = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
reset();
};
const onSubmit: SubmitHandler<UserFormSchema> = (data) => {
console.log(data);
};
return (
<>
<form action="#" onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name">氏名</label>
<input id="name" {...register('name')} />
<div>{errors.name?.message}</div>
</div>
<div>
<label htmlFor="email">メールアドレス</label>
<input id="email" {...register('email')} />
<div>{errors.email?.message}</div>
</div>
<div>
<label htmlFor="sex">性別</label>
<select {...register('sex')}>
{Object.entries(sex).map(([key, value], index) => (
<option key={index} value={key}>
{value}
</option>
))}
</select>
<div>{errors.sex?.message}</div>
</div>
<div>
<label htmlFor="role">ロール</label>
<select id="role" {...register('role')}>
{Object.entries(userRole).map(([key, value], index) => (
<option key={index} value={key}>
{value}
</option>
))}
</select>
<div>{errors.role?.message}</div>
</div>
<div>
<label htmlFor="agreement">利用規約</label>
<input id="agreement" type="checkbox" {...register('agreement')} />
<div>{errors.agreement?.message}</div>
</div>
<div>
<button>Submit</button>
</div>
<div>
<button onClick={onReset}>Reset</button>
</div>
</form>
</>
);
};
export default SampleReactHookYupResolver;
まとめ
以上です。
覚えることが多いな! って印象でした。
ユーザビリティが高いフォームを作っている皆さんに感謝😇