Skip to content

Instantly share code, notes, and snippets.

@sheepla
Last active June 15, 2025 13:03
Show Gist options
  • Save sheepla/469bc23ef5eb46152a69353072253997 to your computer and use it in GitHub Desktop.
Save sheepla/469bc23ef5eb46152a69353072253997 to your computer and use it in GitHub Desktop.
MUI + React Hook Formを使ったお問い合わせフォームの実装
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import {
Alert,
Button,
Checkbox,
CircularProgress,
FormControlLabel,
MenuItem,
Snackbar,
TextField,
Typography,
} from "@mui/material";
type FormValues = {
name: string;
email: string;
age: number;
topic: string;
message: string;
agree: boolean;
};
const sleep = async (delay: number) => await new Promise(resolve => setTimeout(resolve, delay))
export default function ContactForm() {
const [loading, setLoading] = useState(false);
const [snackbarOpen, setSnackbarOpen] = useState(false)
const { control, handleSubmit, formState: { errors }, reset } = useForm<
FormValues
>({
defaultValues: {
name: "",
email: "",
age: 0,
topic: "",
message: "",
agree: false,
},
});
const onSubmit = async (data: FormValues) => {
console.log("送信データ:", data);
setLoading(true)
// スリープして擬似的に送信処理をシミュレート
await sleep(1000)
setLoading(false);
setSnackbarOpen(true);
reset(); // フォームをリセット
};
return (
<>
<form
onSubmit={handleSubmit(onSubmit)}
style={{ maxWidth: 500, width: "100%" }}
>
<Controller
name="name"
control={control}
rules={{
required: "お名前は必須です",
minLength: { value: 2, message: "2文字以上で入力してください" },
}}
render={({ field }) => (
<TextField
size="small"
{...field}
label="お名前"
fullWidth
margin="normal"
error={!!errors.name}
helperText={errors.name?.message}
/>
)}
/>
<Controller
name="email"
control={control}
rules={{
required: "メールアドレスは必須です",
pattern: {
value: /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/,
message: "メールアドレスの形式が正しくありません",
},
}}
render={({ field }) => (
<TextField
size="small"
{...field}
label="メールアドレス"
fullWidth
margin="normal"
error={!!errors.email}
helperText={errors.email?.message}
/>
)}
/>
<Controller
name="age"
control={control}
rules={{
required: "年齢は必須です",
min: { value: 18, message: "18歳以上である必要があります" },
max: { value: 120, message: "正しい年齢を入力してください" },
}}
render={({ field }) => (
<TextField
size="small"
{...field}
label="年齢"
type="number"
fullWidth
margin="normal"
error={!!errors.age}
helperText={errors.age?.message}
/>
)}
/>
<Controller
name="topic"
control={control}
rules={{ required: "お問い合わせ種別を選択してください" }}
render={({ field }) => (
<TextField
size="small"
{...field}
select
label="お問い合わせ種別"
fullWidth
margin="normal"
error={!!errors.topic}
helperText={errors.topic?.message}
>
<MenuItem value="support">サポート</MenuItem>
<MenuItem value="feedback">フィードバック</MenuItem>
<MenuItem value="other">その他</MenuItem>
</TextField>
)}
/>
<Controller
name="message"
control={control}
rules={{
required: "お問い合わせ内容は必須です",
minLength: { value: 10, message: "10文字以上入力してください" },
}}
render={({ field }) => (
<TextField
size="small"
{...field}
label="お問い合わせ内容"
multiline
rows={4}
fullWidth
margin="normal"
error={!!errors.message}
helperText={errors.message?.message}
/>
)}
/>
<Controller
name="agree"
control={control}
rules={{ validate: (value) => value || "規約に同意する必要があります" }}
render={({ field }) => (
<FormControlLabel
control={<Checkbox {...field} checked={field.value} />}
label="規約に同意します"
/>
)}
/>
{errors.agree && (
<Typography sx={{ color: "error", margin: 0 }}>{errors.agree.message}</Typography>
)}
<Button
type="submit"
variant="contained"
fullWidth
sx={{ mt: 2, position: 'relative' }}
disabled={loading}
>
{loading && (
<CircularProgress
size={24}
sx={{
color: 'white',
position: 'absolute',
top: '50%',
left: '50%',
marginTop: '-12px',
marginLeft: '-12px',
}}
/>
)}
{loading ? '送信中...' : '送信'}
</Button>
</form>
<Snackbar
open={snackbarOpen}
autoHideDuration={3000}
onClose={() => setSnackbarOpen(false)}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<Alert onClose={() => setSnackbarOpen(false)} severity="success" sx={{ width: '100%' }}>
お問い合わせを送信しました!
</Alert>
</Snackbar>
</>
);
}
@sheepla
Copy link
Author

sheepla commented Jun 15, 2025

実際の画面

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment