ロゴWeb開発ブログ

React(Next.js) + Material UI + React Hook Formでお問い合わせフォームを作る

更新
作成
  • 使用したバージョン
  • Next.js 13.5.6
  • @mui/material 5.14.14
  • react-hook-form 7.47.0

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を使ってお問い合わせを送信する