はじめに
こんにちは、H×Hのセンリツ大好きエンジニアです。(同担OKです。)
今回はタイトル通りフォームを作っていきます。
email
とpassword
、confirmPassword
を使用したサインアップフォームを想定しています。
間違えてる箇所があるかも知れませんが、生暖かい目で見守りつつ教えて下さい。
使用するライブラリ
react-hook-form
フォームのパフォーマンスを向上させ、コードの行数を減らすことを目的としたライブラリです。これにより、不要なリレンダリングを減らし、ユーザー入力を効率的に収集・管理することができます。
zod
TypeScriptでの型安全なデータバリデーションを可能にするライブラリです。zodを使用すると、フォームの入力値に対して厳格な型チェックを行い、開発者が期待するデータ構造を保証できます。
実装
Zodを用いたバリデーションスキーマの作成
import { z } from 'zod'
export const SignUpFormSchema = z
.object({
email: z
.string()
.min(1, { message: 'メールアドレスを入力して下さい。' })
.email({ message: 'メールアドレスの形式で入力して下さい。' })
.max(100, { message: 'メールアドレスの形式で入力して下さい。' }),
password: z
.string()
.min(1, { message: 'パスワードを入力して下さい。' })
.max(100, { message: 'パスワードは100文字以内で入力して下さい。' }),
confirmPassword: z
.string()
.min(1, { message: 'パスワードを再入力して下さい。' })
.max(100, { message: 'パスワードは100文字以内で入力して下さい。' })
})
.superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
message: 'パスワードが異なります。',
path: ['confirmPassword'],
code: z.ZodIssueCode.custom
})
}
})
export type SignUpFormType = z.infer<typeof SignUpFormSchema>
このファイルでは、以下のことを行なっています。
- バリデーションに
email
とpassword
、そしてconfirmPassword
フィールドを設定 - それぞれに対して最小文字数、最大文字数の検証ルールを適用
-
email
はメールアドレス形式のルールも追加
-
-
superRefine
メソッドを使用して、passwordとconfirmPasswordが一致しているかどうかのカスタムバリデーションを適用
ここで定義したバリデーションスキーマを、react-hook-formに適用させます。
'use client'
import { SignUpFormSchema, SignUpFormType } from '@/schemas/SignUpFormSchema'
import { useForm, FormProvider } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
export const SignUpPage = () => {
const methods = useForm<SignUpFormType>({
mode: 'onChange',
reValidateMode: 'onChange',
resolver: zodResolver(SignUpFormSchema),
defaultValues: {
email: '',
password: '',
confirmPassword: ''
}
})
const {
register,
formState: { errors },
handleSubmit
} = methods
ここでは、バリデーションスキーマを適用しつつ、フォームの状態を管理するための設定を行います。
-
react-hook-form
のuseForm
フックを使用- フォームの状態管理とバリデーションを簡単にするためのフック
-
mode: 'onChange'
とreValidateMode: 'onChange'
でバリデーションタイミングをフォームの値が変更される度に行う -
resolver: zodResolver(SignUpFormSchema)
でreact-hook-form
のバリデーションロジックに先ほど追加したカスタムバリデーションリゾルバを設定 - 初期値設定(全て空)
-
useForm
フックから得られたmethods
オブジェクトを分割代入
最後に、フォームのUIとSubmit時の挙動を作っていくう!(魚捌きおじさん風)
UI部分はChakraUIを採用しています。(あまりCSSを書きたくないため)
import { Button, Container } from '@chakra-ui/react'
import { FormInput } from '@/components/atoms/FormInput'
import { SignUp } from '@/api/user'
import { STATUS_CODE } from '@/const'
interface FormData {
email: string
password: string
confirmPassword: string
}
export const SignUpPage = () => {
~~~~~ 先ほどの続き ~~~~~
const onSubmit = async (params: FormData) => {
console.log(params)
}
return (
<Container size="md">
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
<FormInput
type="email"
register={register('email')}
label="メールアドレス"
placeholder="example@example.com"
errMessage={errors.email?.message}
/>
<FormInput
type="password"
register={register('password')}
label="パスワード"
placeholder="パスワードを入力"
errMessage={errors.password?.message}
/>
<FormInput
type="password"
register={register('confirmPassword')}
label="確認用パスワード"
placeholder="パスワードを再入力"
errMessage={errors.confirmPassword?.message}
/>
<Button type="submit">
新規登録
</Button>
</form>
</FormProvider>
</Container>
)
}
コードの解説は以下の通りです。
-
onSubmit
は、フォームが送信されるタイミングで呼び出され、SignUp
APIを非同期で呼び出す-
SignUp
は以下のように作成していますが、よしなに作って下さい。- 引数:
email
、password
- 返り値:
status
、data
- 引数:
-
status
によって挙動を変更- モーダルを使って送信成功、失敗のUIを呼び出すようにしても良いかと思います。
-
-
<FormProvider {...methods}>
は、react-hook-form
のFormProvider
を使用して、フォームの状態管理に必要なmethodsオブジェクトをフォーム内の全てのコンポーネントに渡す- これにより、フォーム内のどのコンポーネントからもフォームの状態や機能にアクセスできる
-
<form onSubmit={handleSubmit(onSubmit)}>
は、フォームのonSubmit
イベントをhandleSubmit
関数に委譲- フォームが送信される前に
react-hook-form
によるバリデーションが行われ、バリデーションをパスしたデータのみがonSubmit
関数に渡される
- フォームが送信される前に
-
<FormInput ... />
コンポーネントは、メールアドレス、パスワード、確認用パスワードの各入力フィールドをレンダリング-
react-hook-form
のregister
関数を使用してフォームの状態管理とバリデーションに統合される
-
FormInput
は自分が作ったコンポーネントなので、以下に実装を書いておきますが、ChakraUIのInput
コンポーネントをそのまま使用しても良いと思います。
import { Box, FormControl, FormErrorMessage, Input } from '@chakra-ui/react'
import { HTMLInputTypeAttribute } from 'react'
import { UseFormRegisterReturn } from 'react-hook-form'
interface FormInputProps {
label: string
type?: HTMLInputTypeAttribute
placeholder?: string
register?: UseFormRegisterReturn
errMessage?: string
}
export const FormInput = (props: FormInputProps) => {
const { label, type = 'text', placeholder, register, errMessage } = props
return (
<Box>
<FormControl isInvalid={!!errMessage}>
{label}
<Input type={type} {...register} placeholder={placeholder} />
{!!errMessage && <FormErrorMessage>{errMessage}</FormErrorMessage>}
</FormControl>
</Box>
)
}
以上で、フォームの作成が完了しました。
動作確認してみます。見た目にはツッコミを入れないでください。
初期状態では、placeholderのみ表示されています。
バリデーションに引っ掛かるようにフォームに入力してみます。
このように、きちんとバリデーションチェックが行われているのがわかります。
コンソールに、入力した値が正しく表示されていることが分かります。
これでいとも簡単にフォームを錬金できましたね🎵
最終的なコードです。
'use client'
import { SignUpFormSchema, SignUpFormType } from '@/schemas/SignUpFormSchema'
import { useForm, FormProvider } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { Button, Container } from '@chakra-ui/react'
import { FormInput } from '@/components/atoms/FormInput'
export const SignUpPage = () => {
const methods = useForm<SignUpFormType>({
mode: 'onChange',
reValidateMode: 'onChange',
resolver: zodResolver(SignUpFormSchema),
defaultValues: {
email: '',
password: '',
confirmPassword: ''
}
})
const {
register,
formState: { errors },
handleSubmit
} = methods
const onSubmit = async (params: FormData) => {
console.log(params)
}
return (
<Container size="md">
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
<FormInput
type="email"
register={register('email')}
label="メールアドレス"
placeholder="example@example.com"
errMessage={errors.email?.message}
/>
<FormInput
type="password"
register={register('password')}
label="パスワード"
placeholder="パスワードを入力"
errMessage={errors.password?.message}
/>
<FormInput
type="password"
register={register('confirmPassword')}
label="確認用パスワード"
placeholder="パスワードを再入力"
errMessage={errors.confirmPassword?.message}
/>
<Button type="submit">
新規登録
</Button>
</form>
</FormProvider>
</Container>
)
}
おまけ(送信ボタンの活性/非活性化)
react-hook-form
のwatch
またはuseWatch
とgetFieldState
を使って、フォームの値が正常な場合のみ送信ボタンが活性化するように実装します。
~~~ 省略 ~~~
const {
register,
formState: { errors },
handleSubmit,
getFieldState,
control
} = methods
const watchEmail = useWatch({
control,
name: 'email'
})
const watchPassword = useWatch({
control,
name: 'password'
})
const watchConfirmPassword = useWatch({
control,
name: 'confirmPassword'
})
// 必須入力の項目が全て正しく入力されているかチェック
const isDisabled = (): boolean => {
let isDisabled = false
if (getFieldState('email').invalid || !watchEmail) {
isDisabled = true
}
if (getFieldState('password').invalid || !watchPassword) {
isDisabled = true
}
if (getFieldState('confirmPassword').invalid || !watchConfirmPassword) {
isDisabled = true
}
return isDisabled
}
~~~ 省略 ~~~
<Button type="submit" isDisabled={isDisabled()}>
新規登録
</Button>
このように、methods
から追加でgetFieldState
とcontrol
を分割代入します。
そして、各フォームの監視用関数を定義し値が入っていない場合もしくはバリデーションチェックに引っ掛かっている場合Falseを返す関数を作成します。
(watch
を使うともう少し短く書けますが、watch
はフォーム全体に再レンダリングが走ってしまうのでパフォーマンス的によろしくないです。)
その関数を、ボタンコンポーネントのisDisabled
に渡すと実装できます。
さいごに
ここまで読んでいただき感謝です。
いえ、感謝では足りません。嬉しいYummy感謝感謝🎵です。(真顔)
何か不明な点や改善点等ありましたら気軽にコメントしていただけると幸いです。
それではごきげんよう、センリツでした。