React(Next.js)とMaterial UIとReact Hook Formを使って次のようなお問い合わせフォームを作っていく。
(送信ボタンを押しても実際には送信されないので自由にいじってOK!)
今回は名前(name)は任意入力でメールアドレス(email)と内容(message)は必須入力とした。
export type FormValuesType = { name?: string; email: string; message: string;};
入力欄をMUIのTextFieldで作成していく。
import { TextField } from "@mui/material";import { UseFormRegister } from "react-hook-form";export function NameField(props: { register: UseFormRegister<FormValuesType>;}) { return( <TextField label="お名前" variant="filled" helperText=' ' {...props.register('name')} /> )};
propsのregisterはReact Hook Formと紐づけるための関数。
第一引数にFormValuesTypeのキーのいずれかを設定する。
export function EmailField(props: { register: UseFormRegister<FormValuesType>; errorMessage?: string;}) { return ( <TextField label="メールアドレス(必須)" type='email' variant="filled" error={props.errorMessage !== undefined} helperText={props.errorMessage || ' '} {...props.register('email', { required: 'メールアドレスを入力してください', pattern: { value: /\S+@\S+\.\S+/, message: "無効なメールアドレスです", } })} /> );};
propsのerrorMessageには入力エラーがなければundefined、あればエラーメッセージが渡される。
registerの第二引数にはオプションを渡す。
メールアドレスを必須項目にして入力していなければ「メールアドレスを入力してください」と表示させる。
メールアドレスと違う形式の文字列(abcde@abcなど)が入力されたら「無効なメールアドレスです」と表示させる。
helperTextにはエラーメッセージを渡しているが、メッセージがなければ空白文字列を渡している。
これはエラーメッセージ要素の高さを一定にさせるためのもの。
export function MessageField(props: { register: UseFormRegister<FormValuesType>; errorMessage?: string;}) { return ( <TextField multiline rows={6} label="お問い合わせ内容(必須)" variant="filled" error={props.errorMessage !== undefined} helperText={props.errorMessage || ' '} {...props.register('message', { required: 'お問い合わせ内容を入力してください' })} /> );};
TextFieldにmultilineとrowsを指定して6行ぶんの高さに設定している。
'use client'; //Next.jsでは必要import { Stack, Button, Box } from '@mui/material';import { useForm } from 'react-hook-form';import { NameField, EmailField, MessageField } from './inputFields';export function ContactForm(props: { onSubmit: (formValues: FormValuesType) => void; isLoading: boolean;}) { const { handleSubmit, formState: { errors }, register } = useForm<FormValuesType>(); return ( <form noValidate onSubmit={handleSubmit(props.onSubmit)}> <Stack spacing={2} sx={{ maxWidth: 'sm', margin: 'auto', marginTop: 8 }}> <NameField register={register} /> <EmailField register={register} errorMessage={errors.email?.message} /> <MessageField register={register} errorMessage={errors.message?.message} /> <Box textAlign='right'> <Button variant="contained" type='submit' disabled={props.isLoading} children='送信' /> </Box> </Stack> </form> );};
propsのonSubmitは送信ボタンが押されて、入力エラーもないときに実行される関数。
引数には入力されたデータが渡される。
useFormの戻り値であるerrorsには入力エラーがある項目のエラーメッセージが入る。
例えばメールアドレスで入力形式エラーがあったらerrorsオブジェクトは次のようになる。
{ email: { type: 'pattern', message: '無効なメールアドレスです', ... }};
入力欄にerrorMessage={errors.message?.message}
とすることでエラーメッセージがあればそのメッセージを、なければundifinedが渡される。
入力のバリデーションはReact Hook Formで行うのでformに備わっているバリデーション機能を無効化するためformにnoValidateを指定している。
propsのisLoadingはデータの送信中はtrueとなるため、送信ボタンを無効化させる。
これでフォームのUIは完成!。
import { Snackbar, Alert } from "@mui/material";export function ErrorSnackbar(props: { hasError: boolean; closeError: () => void;}) { return ( <Snackbar open={props.hasError} autoHideDuration={4000} anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} onClose={props.closeError} > <Alert severity="error" onClose={props.closeError} > 送信に失敗しました </Alert> </Snackbar> );};
propsのhasErrorにtrueが渡されると表示される。
表示後、4秒経過するとpropsのcloseErrorが実行する。
入力されたデータをaxiosやfetchで指定したURLに送る。
ここではごく簡単にfetchで送ることにする。
import { useRouter } from "next/navigation";import { useState } from "react";export const useContactForm = () => { const [hasError, setHasError] = useState(false); const [isLoading, setIsLoading] = useState(false); const router = useRouter(); const closeError = () => setHasError(false); const sendData = async (data: FormValuesType) => { setIsLoading(true); try { const response = await fetch('データを送るURL', { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!response.ok) throw new Error(); router.push('/success'); } catch { setHasError(true); } setIsLoading(false); }; return { hasError, isLoading, sendData, closeError };};
fetchでリクエストを送って帰ってきたレスポンスのステータスが200番代以外だったり、送信自体ができなかったらhasErrorにtrueをセットする。
送信が成功したらrouter.push('サクセスページの相対パス')
を行いサクセスページへ移動する。
これまで作ってきたものを合わせてページを完成させる。
'use client'; //Next.jsでは必要export default function Page() { const { hasError, isLoading, sendData, closeError } = useContactForm(); return ( <div> <h1>お問い合わせ</h1> <ContactForm onSubmit={sendData} isLoading={isLoading} /> <ErrorSnackbar hasError={hasError} closeError={closeError} /> </div> );};
お問い合わせフォームのデータをGoogle reCAPTCHAを利用して安全にサーバーに送る方法はこちらのページで。
React + Django REST framework + Google reCAPTCHA V3を使ってお問い合わせを送信する