React Hook Form触ってみた
React Hook Form チュートリアル
自社プロダクトで使用しているReact Hook Formを習得するため触ってみたことのまとめ
今回、社内のエンジニアにReact Hook Formのチュートリアルを用意していただいたのでそれに沿って進める
問題1から10まであり全てまとめると膨大なため重点を一部抜粋している
フォームの要件
フォームの情報
- 名前(Name)
- メールアドレス(Email)
- パスワード(Password)
- 年齢(Age)
- 趣味(Hobbies)- 複数選択可能
問題
- フォームを初期化し、名前、メールアドレス、パスワード、年齢のデフォルト値を設定してください。
- 名前、メールアドレス、パスワードの入力フィールドをフォームに登録してください。
- 特定の条件(例:ユーザーが特定のチェックボックスをオンにした場合)で、パスワードフィールドの登録を解除してください。
- 各フィールドのバリデーションエラーを表示してください。
- メールアドレスの入力値をリアルタイムで監視し、コンソールに表示してください。
- フォームの送信を処理し、送信データをコンソールに表示してください。
- 送信ボタンの隣にリセットボタンを設置し、フォームの入力値を初期値に戻してください。
- ボタンクリックにより、任意のフィールドにエラーを設定し、もう一度クリックでエラーをクリアしてください。
- 任意のボタンを用意し、そのボタンをクリックすることでメールアドレスフィールドにプリセットのメールアドレスを設定してください
- 趣味(Hobbies)のフィールドを動的に追加・削除できるようにしてください。趣味はテキスト入力で、複数追加可能とします。
1. フォームの初期化
- フォームを初期化し、名前、メールアドレス、パスワード、年齢のデフォルト値を設定してください。
useForm
useFormとはReact Hook Formに備わっているフォームを簡単に管理するためのカスタムフック
様々なメソッドが戻り値に用意されている
defaultValues
defaultValuesを使うとフォームの初期値を設定できる
useForm({
defaultValues: {
name: 'Name',
email: 'test@gmail.com',
password: 'passowrd',
age: '20',
},
});
register
useFormの戻り値であるregisterメソッドを使用するとinput要素の登録ができる
const { onChange, onBlur, name, ref } = register('name') ;
<input
onChange={onChange}
onBlur={onBlur}
name={name}
ref={ref}
/>
// 以下の様に省略も可能
<input {...register('name')} />
問題1の完成系のコード
import './App.css';
import { useForm, useFieldArray } from 'react-hook-form';
function App() {
const {
register,
handleSubmit,
} = useForm({
defaultValues: {
name: 'Name',
email: 'test@gmail.com',
password: 'passowrd',
age: '20',
},
});
const onSubmit = (data: any) => {
console.log(data);
};
return (
<>
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<div>
<label htmlFor="">名前</label>
<input
{...register('name')}
type="text"
/>
</div>
<div>
<label htmlFor="">メールアドレス</label>
<input
{...register('email')}
type="email"
/>
</div>
<div>
<label htmlFor="">パスワード</label>
<input
{...register('password')}
type="password"
/>
</div>
<div>
<label htmlFor="">年齢</label>
<input
{...register('age')}
type="number"
/>
</div>
<div>
<button type="submit">送信</button>
</div>
</div>
</form>
</>
);
}
export default App;
2. エラーの表示
- 各フィールドのバリデーションエラーを表示してください。
formState: { errors }
useFormの戻り値であるformStateにはフォーム全体の状態に関する情報が含まれている
その中のerrorsを使うとバリデーションエラー等を表示できる
registerの第二引数にオブジェクト形式でバリデーション項目とエラーメッセージを設定できる
validateを使うとコールバック関数を引数として渡して検証することも可能
export const FORM_VALIDATE_MESSAGE = {
required: 'この項目は必須です',
emailPattern: '正しいメールアドレスを入力してください。',
} as const;
const validateEmail = (value: string) => {
const re =
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
const isFormatValid = re.test(value);
const isInvalidDotSyntax =
/^[.]/.test(value) || /\.{2,}/.test(value) || /\.@/.test(value);
return isFormatValid && !isInvalidDotSyntax;
};
const {
register,
formState: { errors },
} = useForm({
defaultValues: {
name: 'Name',
email: 'test@gmail.com',
password: 'passowrd',
age: '20',
checkbox: false,
hobbie: 'test',
},
});
<div>
<label htmlFor="">名前</label>
<input
{...register('name', { required: 'Name is required' })}
type="text"
/>
{errors.name && <p>{errors.name.message}</p>}
</div>
<div>
<label htmlFor="">メールアドレス</label>
<input
{...register('email', {
validate: (value) => {
if (!value) {
return FORM_VALIDATE_MESSAGE.required;
}
if (!validateEmail(value)) {
return FORM_VALIDATE_MESSAGE.emailPattern;
}
},
})}
type="email"
/>
{errors.email && <p>{errors.email.message}</p>}
</div>
mode: 'onChange'
そのまま使うと送信ボタンを押した時のみエラーが表示される仕様になっている
入力(onChange)中にエラーが表示されるようにしたい場合はmode: 'onChange'を指定すると良い
const {
register,
formState: { errors },
} = useForm({
defaultValues: {
name: 'Name',
email: 'test@gmail.com',
password: 'passowrd',
age: '20',
checkbox: false,
hobbie: 'test',
},
mode: 'onChange',
});
3. プリセットの設定
- 任意のボタンを用意し、そのボタンをクリックすることでメールアドレスフィールドにプリセットのメールアドレスを設定してください
任意のプリセットとボタンを用意する
プリセット
const presetEmail = 'preset@test.com';
ボタン
<button type="button">
メールアドレスプリセット
</button>
setValue
useFormの戻り値であるsetValueメソッドを使用すると任意の値をフォームに挿入できる
<button
type="button"
onClick={() => setValue('email', presetEmail)}
>
メールアドレスプリセット
</button>
4. フィールドの動的追加・削除
- 趣味(Hobbies)のフィールドを動的に追加・削除できるようにしてください。趣味はテキスト入力で、複数追加可能とします。
useFieldArray
useFieldArrayとはフィールド配列 (動的フォーム) を操作するためのカスタムフック
フォームを動的に操作したい場合はuseFormではなくこちらを使う
今回はフォームの追加、削除を行うためappend、remove、fieldsを取り出す
const { append, remove, fields } = useFieldArray();
引数にはフィールドのnameを設定できる。今回は"hobbie"
useFormから取り出したcontrolも渡す。
const { control } = useForm();
const { append, remove, fields } = useFieldArray({
control,
name: 'hobbie',
});
フィールド追加ボタンとフィールドを用意する
フィールド追加ボタン
<button type="button">
+
</button>
フィールド
fieldsにフォームが配列で格納されているのでmapで1つずつ表示させる
registerの引数にフィールド名.${index}.value
を指定する
<div>
{fields.map((field, index) => (
<div>
<label>
趣味
<input
key={field.id}
{...register(`hobbie.${index}.value`)}
/>
</label>
</div>
))}
</div>
ボタンのonClick関数を設定する
appendを使うことで、ボタンクリックするごとにhobbieのinputを増やせる
const handleClickAddInput = () => {
append({ name: 'hobbie' });
};
<button type="button" onClick={() => handleClickAddInput()}>
+
</button>
削除ボタン実装
ボタンのonClickにremove(index)
を指定する
ボタンをクリックするとinputを削除できる
<button type="button" onClick={() => remove(index)}>
❌
</button>
全体の完成系コード
今回は問題1,4,9,10のみ抜粋したが、全ての問題を含めたコードを以下に記載する
import './App.css';
import { useForm, useFieldArray } from 'react-hook-form';
import { useEffect } from 'react';
export const FORM_VALIDATE_MESSAGE = {
required: 'この項目は必須です',
emailPattern: '正しいメールアドレスを入力してください。',
} as const;
const validateEmail = (value: string) => {
const re =
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
const isFormatValid = re.test(value);
const isInvalidDotSyntax =
/^[.]/.test(value) || /\.{2,}/.test(value) || /\.@/.test(value);
return isFormatValid && !isInvalidDotSyntax;
};
const presetEmail = 'preset@test.com';
function App() {
const {
register,
unregister,
handleSubmit,
watch,
setError,
clearErrors,
formState: { errors },
reset,
setValue,
control,
} = useForm({
defaultValues: {
name: 'Name',
email: 'test@gmail.com',
password: 'passowrd',
age: '20',
checkbox: false,
hobbie: 'test',
},
mode: 'onChange',
});
const { append, remove, fields } = useFieldArray({
control,
name: 'hobbie',
});
const onSubmit = (data: any) => {
console.log(data);
};
const watchCheckbox = watch('checkbox', false);
const watchEmail = watch('email');
const handleClickErrorButton = () => {
if (errors.name) {
clearErrors();
} else {
setError('name', {
message: 'エラー',
});
}
};
const handleClickAddInput = () => {
console.log('input');
append({ name: 'hobbie' });
};
useEffect(() => {
console.log(watchEmail);
}, [watchEmail]);
useEffect(() => {
if (watchCheckbox) {
unregister('password');
}
}, [watchCheckbox, unregister]);
return (
<>
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<div>
<label htmlFor="">名前</label>
<input
{...register('name', { required: 'Name is required' })}
type="text"
/>
{errors.name && <p>{errors.name.message}</p>}
</div>
<div>
<label htmlFor="">メールアドレス</label>
<input
{...register('email', {
validate: (value) => {
if (!value) {
return FORM_VALIDATE_MESSAGE.required;
}
if (!validateEmail(value)) {
return FORM_VALIDATE_MESSAGE.emailPattern;
}
},
})}
type="email"
/>
{errors.email && <p>{errors.email.message}</p>}
</div>
<div>
{!watchCheckbox && (
<>
<label htmlFor="">パスワード</label>
<input
{...register('password', {
required: 'パスワードを入力してください。',
minLength: {
value: 8,
message: 'パスワードは8文字以上で入力してください。',
},
maxLength: {
value: 16,
message: 'パスワードは16文字以内で入力してください。',
},
})}
type="password"
/>
{errors.password && <p>{errors.password.message}</p>}
</>
)}
</div>
<div>
<input {...register('checkbox')} type="checkbox" />
</div>
<div>
<label htmlFor="">年齢</label>
<input
{...register('age', { required: 'age is required' })}
type="number"
/>
{errors.age && <p>{errors.age.message}</p>}
</div>
<div>
<button type="submit">送信</button>
<button type="button" onClick={() => reset()}>
リセット
</button>
<button type="button" onClick={() => handleClickErrorButton()}>
{errors.name ? 'エラーリセット' : 'エラー設定'}
</button>
<button
type="button"
onClick={() => setValue('email', presetEmail)}
>
メールアドレスプリセット
</button>
</div>
</div>
<div>
<button type="button" onClick={() => handleClickAddInput()}>
+
</button>
<div>
{fields.map((field, index) => (
<div>
<label>
趣味
<input
key={field.id} // important to include key with field's id
{...register(`hobbie.${index}.value`)}
/>
</label>
<button type="button" onClick={() => remove(index)}>
❌
</button>
</div>
))}
</div>
</div>
</form>
</>
);
}
export default App;
Discussion
ContextのProviderでグローバルな読宣の機能を与えるとともに、Flag設定の更新はRHFで管轄するといった形になります
問題にあるかは不明ですが、Feature Flagのユースケースもバリエーションに加えると、いいかもです
demo code.