✅ はじめに
こんにちは!このシリーズでは、React + Vite + TypeScript + MUIを使ってモダンなWebアプリを開発する方法をステップバイステップで解説しています。
第8回となる今回は、React Hook Form と MUI を使って、ユーザーの入力体験を向上させる ステップフォーム(Step Form) を実装します。
フォームを複数のページに分けて入力させることで、長いフォームでもストレスなく入力できる UI を実現します。
📦 使用する技術スタック
| ツール | バージョン | 説明 |
|---|---|---|
| React Hook Form | v7.x | 高性能なフォームバリデーションライブラリ |
| MUI | v5.x | マテリアルデザインベースのUIコンポーネント |
| TypeScript | 5.x | 型安全なJavaScript |
| Vite | 5.x | 高速な開発環境とビルドツール |
🎯 今回のゴール
- フォームの入力を段階的に表示
- 各ステップでバリデーション
- 最後に確認画面と送信
🚀 実装手順
1. プロジェクトの作成
まずは、Viteを使って新しいプロジェクトを作成します:
npm create vite@latest react-step-form -- --template react-ts cd react-step-form npm install
2. 必要なパッケージのインストール
MUIとReact Hook Formをインストールします:
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material react-hook-form
3. プロジェクト構成
今回のプロジェクトは以下のような構成になります:
src/ ├── components/ │ ├── StepOne.tsx │ ├── StepTwo.tsx │ └── Confirmation.tsx ├── StepForm.tsx └── App.tsx
4. 型定義の作成
まず、共通で使用する型を定義します:
type FormValues = {
name: string;
email: string;
};
5. 各ステップコンポーネントの作成
src/components/StepOne.tsx(名前入力):
import { TextField } from "@mui/material";
import { useFormContext } from "react-hook-form";
import type { FC } from "react";
import type { FormValues } from "../types";
const StepOne: FC = () => {
const {
register,
formState: { errors },
} = useFormContext<FormValues>();
return (
<TextField
fullWidth
label="名前"
{...register("name", { required: "名前は必須です" })}
error={!!errors.name}
helperText={errors.name?.message}
/>
);
};
export default StepOne;
src/components/StepTwo.tsx(メール入力):
import { TextField } from "@mui/material";
import { useFormContext } from "react-hook-form";
import type { FC } from "react";
import type { FormValues } from "../types";
const StepTwo: FC = () => {
const {
register,
formState: { errors },
} = useFormContext<FormValues>();
return (
<TextField
fullWidth
label="メール"
{...register("email", {
required: "メールは必須です",
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: "メール形式が不正です",
},
})}
error={!!errors.email}
helperText={errors.email?.message}
/>
);
};
export default StepTwo;
src/components/Confirmation.tsx(確認画面):
import { Typography } from "@mui/material";
import { useFormContext } from "react-hook-form";
import type { FC } from "react";
import type { FormValues } from "../types";
const Confirmation: FC = () => {
const { getValues } = useFormContext<FormValues>();
const { name, email } = getValues();
return (
<>
<Typography>名前: {name}</Typography>
<Typography>メール: {email}</Typography>
</>
);
};
export default Confirmation;
6. メインフォームコンポーネントの作成
src/StepForm.tsx:
import {
Box,
Button,
Container,
Step,
StepLabel,
Stepper,
Typography,
} from "@mui/material";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import StepOne from "./components/StepOne";
import StepTwo from "./components/StepTwo";
import Confirmation from "./components/Confirmation";
import type { FC } from "react";
import type { FormValues } from "./types";
const steps = ["名前入力", "メール入力", "確認"] as const;
const StepForm: FC = () => {
const methods = useForm<FormValues>({
defaultValues: {
name: "",
email: "",
},
});
const [step, setStep] = useState(0);
const onNext = async () => {
const valid = await methods.trigger();
if (valid) setStep((prev) => prev + 1);
};
const onBack = () => setStep((prev) => prev - 1);
const onSubmit = (data: FormValues) => {
console.log(JSON.stringify(data, null, 2));
};
const renderStep = () => {
switch (step) {
case 0:
return <StepOne />;
case 1:
return <StepTwo />;
case 2:
return <Confirmation />;
default:
return null;
}
};
return (
<FormProvider {...methods}>
<Container maxWidth="sm">
<Box mt={5}>
<Typography variant="h5" gutterBottom>
ステップフォーム
</Typography>
<Stepper activeStep={step}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
<Box mt={4} mb={2}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Box display="flex" flexDirection="column" gap={2}>
{renderStep()}
<Box display="flex" justifyContent="space-between">
{step > 0 && (
<Button variant="outlined" onClick={onBack}>
戻る
</Button>
)}
{step < steps.length - 1 ? (
<Button variant="contained" onClick={onNext}>
次へ
</Button>
) : (
<Button type="submit" variant="contained" color="primary">
送信
</Button>
)}
</Box>
</Box>
</form>
</Box>
</Box>
</Container>
</FormProvider>
);
};
export default StepForm;
7. App.tsxの更新
最後に、src/App.tsxを更新してフォームを表示します:
import StepForm from "./StepForm";
import type { FC } from "react";
const App: FC = () => {
return <StepForm />;
};
export default App;
🎯 主要なポイント
1. フォーム状態の共有
- FormProvider: すべてのステップでフォームの状態を共有
- useFormContext: 子コンポーネントからのフォーム操作を簡単に
- 型安全性: TypeScriptによる厳密な型チェック
2. ステップ管理
- useState: 現在のステップを管理
- trigger(): 次のステップへ進む前にバリデーション
- 条件付きレンダリング: 現在のステップに応じたコンポーネント表示
3. UI/UX設計
- Stepper: 進捗状況を視覚的に表示
- ナビゲーション: 戻る/次へ/送信ボタンの適切な表示
- バリデーション: 各ステップでの入力チェック
🔚 まとめ
今回は、React Hook Form × MUI を用いて複数ステップで構成されるフォームを作成しました。これにより、ユーザーにとって負担の少ない入力体験が提供できます。
📚 次回予告
【第9回】React Hook Form:フォーム入力内容のローカルストレージ保存