✅ はじめに
こんにちは!このシリーズでは、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:フォーム入力内容のローカルストレージ保存