2025年6月1日日曜日

【第9回】React Hook Form:フォーム入力内容のローカルストレージ保存

# 【第9回】React Hook Form:フォーム入力内容のローカルストレージ保存

✅ はじめに

こんにちは!このシリーズでは、React + Vite + TypeScript + MUIを使ってモダンなWebアプリを開発する方法をステップバイステップで解説しています。

第9回となる今回は、React Hook Form を使用して、フォーム入力内容をローカルストレージに保存・復元する機能を実装します。ユーザーが入力途中で画面を閉じても、次回アクセス時に入力内容を復元できる機能を作ります。

📦 使用する技術スタック

ツール バージョン 説明
React Hook Form v7.x 高性能なフォームバリデーションライブラリ
MUI v5.x マテリアルデザインベースのUIコンポーネント
TypeScript 5.x 型安全なJavaScript
Vite 5.x 高速な開発環境とビルドツール

🎯 今回のゴール

  • フォーム入力内容の自動保存機能の実装
  • 保存したデータの復元機能の実装
  • データのクリア機能の実装

🚀 実装手順

1. プロジェクトの作成

まずは、Viteを使って新しいプロジェクトを作成します:

npm create vite@latest react-form-storage -- --template react-ts
cd react-form-storage
npm install

2. 必要なパッケージのインストール

MUIとReact Hook Formをインストールします:

npm install @mui/material @emotion/react @emotion/styled @mui/icons-material react-hook-form

3. カスタムフックの作成

src/hooks/useLocalStorage.tsを作成します:

import { useState, useEffect } from "react";

export function useLocalStorage(key: string, initialValue: T) {
  // 初期値の設定
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  // 値の更新時にローカルストレージも更新
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.error(error);
    }
  }, [key, storedValue]);

  return [storedValue, setStoredValue] as const;
}

4. フォームコンポーネントの作成

src/components/PersistentForm.tsxを作成します:

import { useEffect } from "react";
import { useForm } from "react-hook-form";
import {
  Box,
  Button,
  Container,
  TextField,
  Typography,
} from "@mui/material";
import { useLocalStorage } from "../hooks/useLocalStorage";
import type { FC } from "react";

type FormValues = {
  name: string;
  email: string;
  message: string;
};

const STORAGE_KEY = "form_data";

const PersistentForm: FC = () => {
  const [storedData, setStoredData] = useLocalStorage>(
    STORAGE_KEY,
    {}
  );

  const {
    register,
    handleSubmit,
    reset,
    watch,
    formState: { errors },
  } = useForm({
    defaultValues: storedData,
  });

  // フォームの値が変更されたら自動保存
  useEffect(() => {
    const subscription = watch((value) => {
      setStoredData(value);
    });
    return () => subscription.unsubscribe();
  }, [watch, setStoredData]);

  const onSubmit = (data: FormValues) => {
    console.log("送信データ:", data);
    alert(JSON.stringify(data, null, 2));
    // 送信成功時にフォームとストレージをクリア
    reset();
    setStoredData({});
  };

  const handleClear = () => {
    reset();
    setStoredData({});
  };

  return (
    <Container maxWidth="sm">
      <Box mt={5}>
        <Typography variant="h5" gutterBottom>
          永続化フォーム
        </Typography>
        <form onSubmit={handleSubmit(onSubmit)} noValidate>
          <Box display="flex" flexDirection="column" gap={2}>
            <TextField
              fullWidth
              label="名前"
              {...register("name", { required: "名前は必須です" })}
              error={Boolean(errors.name)}
              helperText={errors.name?.message}
            />
            <TextField
              fullWidth
              label="メール"
              {...register("email", {
                required: "メールは必須です",
                pattern: {
                  value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
                  message: "メール形式が不正です",
                },
              })}
              error={Boolean(errors.email)}
              helperText={errors.email?.message}
            />
            <TextField
              fullWidth
              label="メッセージ"
              multiline
              rows={4}
              {...register("message", {
                required: "メッセージは必須です",
                minLength: {
                  value: 10,
                  message: "10文字以上入力してください",
                },
              })}
              error={Boolean(errors.message)}
              helperText={errors.message?.message}
            />
            <Box
              display="flex"
              justifyContent="space-between"
              alignItems="center"
            >
              <Button
                variant="outlined"
                color="secondary"
                onClick={handleClear}
              >
                クリア
              </Button>
              <Button type="submit" variant="contained" color="primary">
                送信
              </Button>
            </Box>
          </Box>
        </form>
      </Box>
    </Container>
  );
};

export default PersistentForm;

5. App.tsxの更新

最後に、src/App.tsxを更新してフォームを表示します:

import PersistentForm from "./components/PersistentForm";
import type { FC } from "react";

const App: FC = () => {
  return ;
};

export default App;

🎯 主要なポイント

1. カスタムフック設計

  • 型安全性: ジェネリクスを使用した柔軟な型定義
  • エラーハンドリング: try-catchによる安全な処理
  • 永続化: useEffectによる自動保存の実装

2. フォーム機能

  • 自動保存: watchによる入力値の変更検知
  • データ復元: defaultValuesによる初期値の設定
  • クリア機能: フォームとストレージの同時クリア

3. UX設計

  • バリデーション: リアルタイムな入力検証
  • フィードバック: エラーメッセージの表示
  • 操作性: クリアと送信の2つのアクション

🔚 まとめ

今回は、React Hook Form と localStorage を組み合わせて、フォーム入力内容を永続化する機能を実装しました。 この実装により、以下のメリットが得られます:

  • 入力内容の保持: ブラウザを閉じても入力内容が失われない
  • 🔄 自動保存: 入力内容がリアルタイムで保存される
  • 🗑️ データ管理: 必要に応じて入力内容をクリア可能

📚 次回予告

【第10回】React Hook Form:フォームの状態管理とコンテキスト

2025年5月28日水曜日

【第8回】React Hook Form × MUI:ステップフォームの実装

✅ はじめに

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

2025年5月26日月曜日

【第7回】React Hook Form × MUI:フォームをコンポーネント単位で分割する設計手法

# 【第7回】React Hook Form × MUI:フォームをコンポーネント単位で分割する設計手法

✅ はじめに

こんにちは!このシリーズでは、React + Vite + TypeScript + MUIを使ってモダンなWebアプリを開発する方法をステップバイステップで解説しています。

第7回となる今回は、「フォームをコンポーネント単位で分割する設計手法」について解説します。前回作成したフォームをベースに、入力フィールドを個別のコンポーネントに分割し、保守性と再利用性を高める実装方法を学んでいきます。

📦 使用する技術スタック

ツール バージョン 説明
React Hook Form v7.x 高性能なフォームバリデーションライブラリ
MUI v5.x マテリアルデザインベースのUIコンポーネント
TypeScript 5.x 型安全なJavaScript
Vite 5.x 高速な開発環境とビルドツール

🚀 実装手順

1. プロジェクトの作成

まずは、Viteを使って新しいプロジェクトを作成します:

npm create vite@latest react-form-components -- --template react-ts
cd react-form-components
npm install

2. 必要なパッケージのインストール

MUIとReact Hook Formをインストールします:

npm install @mui/material @emotion/react @emotion/styled @mui/icons-material react-hook-form

3. プロジェクト構成

今回のプロジェクトは以下のような構成になります:

src/
  ├── components/
  │   ├── NameField.tsx
  │   └── EmailField.tsx
  ├── FormComponent.tsx
  └── App.tsx

4. 入力フィールドコンポーネントの作成

まず、src/components/NameField.tsxを作成します:

import { TextField } from "@mui/material";
import { useForm } from "react-hook-form";
import type { FC } from "react";

type Props = {
  register: ReturnType<typeof useForm>["register"];
  error?: string;
};

const NameField: FC<Props> = ({ register, error }) => {
  return (
    <TextField
      fullWidth
      label="名前"
      {...register("name", { required: "名前は必須です" })}
      error={Boolean(error)}
      helperText={error}
    />
  );
};

export default NameField;

次に、src/components/EmailField.tsxを作成します:

import { TextField } from "@mui/material";
import { useForm } from "react-hook-form";
import type { FC } from "react";

type Props = {
  register: ReturnType<typeof useForm>["register"];
  error?: string;
};

const EmailField: FC<Props> = ({ register, error }) => {
  return (
    <TextField
      fullWidth
      label="メール"
      {...register("email", {
        required: "メールは必須です",
        pattern: {
          value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
          message: "メール形式が不正です",
        },
      })}
      error={Boolean(error)}
      helperText={error}
    />
  );
};

export default EmailField;

5. メインフォームコンポーネントの作成

src/FormComponent.tsxを作成します:

import { useForm, type FieldValues, type UseFormRegister } from "react-hook-form";
import { Box, Button, Container, Typography } from "@mui/material";
import NameField from "./components/NameField";
import EmailField from "./components/EmailField";
import type { FC } from "react";

type FormValues = {
  name: string;
  email: string;
};

const FormComponent: FC = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>();

  const onSubmit = (data: FormValues) => {
    console.log("送信データ:", data);
    alert(JSON.stringify(data, null, 2));
  };

  return (
    <Container maxWidth="sm">
      <Box mt={5}>
        <Typography variant="h5" gutterBottom>
          コンポーネント分割フォーム
        </Typography>
        <form onSubmit={handleSubmit(onSubmit)} noValidate>
          <Box display="flex" flexDirection="column" gap={2}>
            <NameField
              register={register as unknown as UseFormRegister<FieldValues>}
              error={errors.name?.message}
            />
            <EmailField
              register={register as unknown as UseFormRegister<FieldValues>}
              error={errors.email?.message}
            />
            <Box textAlign="center">
              <Button type="submit" variant="contained" color="primary">
                送信
              </Button>
            </Box>
          </Box>
        </form>
      </Box>
    </Container>
  );
};

export default FormComponent;

6. App.tsxの更新

最後に、src/App.tsxを更新してフォームを表示します:

import FormComponent from "./FormComponent";
import type { FC } from "react";

const App: FC = () => {
  return <FormComponent />;
};

export default App;

🎯 主要なポイント

1. コンポーネント設計

  • 単一責任の原則: 各コンポーネントは1つの役割に特化
  • 再利用性: 入力フィールドを独立したコンポーネントとして実装
  • 型安全性: TypeScriptによる厳密な型チェック

2. Props設計

  • register関数: React Hook Formの登録関数を適切に受け渡し
  • エラー処理: エラーメッセージを親から子へ伝播
  • 型定義: 明確なPropsの型定義による安全性確保

3. フォーム管理

  • 状態管理: useFormによる一元的なフォーム状態管理
  • バリデーション: 各フィールドでの個別バリデーション定義
  • 送信処理: handleSubmitによる統一的な送信処理

🔚 まとめ

今回は、React Hook Formを使用したフォームをコンポーネント単位に分割する設計手法について学びました。この設計により、以下のメリットが得られます:

  • 保守性の向上: 各コンポーネントが独立して管理可能
  • 🔄 再利用性の向上: 共通の入力フィールドを別のフォームでも使用可能
  • 📝 コードの可読性向上: 責任範囲が明確で理解しやすい構造

📚 次回予告

【第8回】React Hook Form × MUI:ステップフォームの実装

【第6回】バリデーション結果をモーダルで表示するReact Hook Form実装

✅ はじめに

こんにちは!このシリーズでは、React + Vite + TypeScript + MUIを使ってモダンなWebアプリを開発する方法をステップバイステップで解説しています。

第6回となる今回は、「バリデーション結果をモーダルで表示するReact Hook 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-form-modal -- --template react-ts
cd react-form-modal
npm install

2. 必要なパッケージのインストール

MUIとReact Hook Formをインストールします:

npm install @mui/material @emotion/react @emotion/styled @mui/icons-material react-hook-form @fontsource/roboto

3. モーダル付きフォームの作成

src/FormWithModal.tsxを作成します:

import { useForm } from "react-hook-form"
import {
  Box,
  Button,
  Container,
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  TextField,
  Typography,
} from "@mui/material"
import { useState } from "react"

type FormValues = {
  name: string
  email: string
}

export default function FormWithModal() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({
    defaultValues: {
      name: "",
      email: "",
    },
  })

  const [open, setOpen] = useState(false)
  const [submittedData, setSubmittedData] = useState<FormValues | null>(null)

  const onSubmit = (data: FormValues) => {
    setSubmittedData(data)
    setOpen(true)
  }

  return (
    <Container maxWidth="sm">
      <Box mt={5}>
        <Typography variant="h5" gutterBottom>
          モーダル表示付きフォーム
        </Typography>
        <form onSubmit={handleSubmit(onSubmit)} noValidate>
          <Box display="flex" flexDirection="column" gap={2}>
            <TextField
              label="名前"
              fullWidth
              {...register("name", { required: "名前は必須です" })}
              error={Boolean(errors.name)}
              helperText={errors.name?.message}
            />
            <TextField
              label="メール"
              fullWidth
              {...register("email", {
                required: "メールは必須です",
                pattern: {
                  value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
                  message: "メール形式が不正です",
                },
              })}
              error={Boolean(errors.email)}
              helperText={errors.email?.message}
            />
            <Button type="submit" variant="contained" color="primary">
              送信
            </Button>
          </Box>
        </form>
      </Box>
      <Dialog open={open} onClose={() => setOpen(false)}>
        <DialogTitle>送信内容の確認</DialogTitle>
        <DialogContent dividers>
          {submittedData && (
            <Box>
              <Typography>名前: {submittedData.name}</Typography>
              <Typography>メール: {submittedData.email}</Typography>
            </Box>
          )}
        </DialogContent>
        <DialogActions>
          <Button onClick={() => setOpen(false)} color="primary">
            閉じる
          </Button>
        </DialogActions>
      </Dialog>
    </Container>
  )
}

4. App.tsxの更新

最後に、src/App.tsxを更新してフォームを表示します:

import { ThemeProvider, createTheme } from '@mui/material'
import FormWithModal from "./FormWithModal"
import '@fontsource/roboto/300.css'
import '@fontsource/roboto/400.css'
import '@fontsource/roboto/500.css'
import '@fontsource/roboto/700.css'

const theme = createTheme({
  typography: {
    fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
  },
})

function App() {
  return (
    <ThemeProvider theme={theme}>
      <FormWithModal />
    </ThemeProvider>
  )
}

export default App

🎯 主要なポイント

1. モーダル管理

  • Dialog コンポーネント: MUIのモーダルダイアログを使用
  • 状態管理: useStateでモーダルの開閉を制御
  • 送信データ: 確認用モーダルに表示するデータを保持

2. フォーム機能

  • バリデーション: 名前とメールアドレスの入力検証
  • エラー表示: フィールド単位でのエラーメッセージ表示
  • 送信処理: フォームデータをモーダルで確認

3. UIコンポーネント

  • Dialog関連: タイトル、コンテンツ、アクションの3層構造
  • TextField: エラー状態とメッセージの統合
  • レイアウト: Boxコンポーネントによる柔軟な配置

🔚 まとめ

今回は、React Hook FormとMUIのDialogコンポーネントを組み合わせて、ユーザーフレンドリーなフォームを実装しました。バリデーション結果や送信内容をモーダルで表示することで、より分かりやすいUIを実現できました。

📚 次回予告

【第7回】React Hook Form × MUI:フォームをコンポーネント単位で分割する設計手法

【第5回】React Hook Formによるフォーム処理の実践 - 複数メールアドレスの処理

✅ はじめに

こんにちは!このシリーズでは、React + Vite + TypeScript + MUIを使ってモダンなWebアプリを開発する方法をステップバイステップで解説しています。

第5回となる今回は、「React Hook Formを使った複数メールアドレスの入力フォーム」を実装します。useFieldArrayを使用した動的なフォーム要素の追加・削除と、効果的なバリデーション処理を学びます。

📦 使用する技術スタック

ツール バージョン 説明
React Hook Form v7.x 高性能なフォームバリデーションライブラリ
MUI v5.x マテリアルデザインベースのUIコンポーネント
TypeScript 5.x 型安全なJavaScript
Vite 5.x 高速な開発環境とビルドツール

🚀 実装手順

1. プロジェクトの作成

まずは、Viteを使って新しいプロジェクトを作成します:

npm create vite@latest react-form-advanced -- --template react-ts
cd react-form-advanced
npm install

2. 必要なパッケージのインストール

MUIとReact Hook Formをインストールします:

npm install @mui/material @emotion/react @emotion/styled @mui/icons-material react-hook-form @fontsource/roboto

3. 複数メール入力フォームの作成

src/AdvancedForm.tsxを作成します:

import type { FC } from "react"
import { useForm, useFieldArray } from "react-hook-form"
import {
  Box,
  Button,
  Container,
  TextField,
  Typography,
} from "@mui/material"
import { Add, Delete } from "@mui/icons-material"

type FormValues = {
  name: string
  emails: { email: string }[]
}

const AdvancedForm: FC = () => {
  const {
    register,
    control,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({
    defaultValues: {
      name: "",
      emails: [{ email: "" }],
    },
  })

  const { fields, append, remove } = useFieldArray({
    control,
    name: "emails",
  })

  const onSubmit = (data: FormValues) => {
    console.log("送信データ:", data)
    alert(JSON.stringify(data, null, 2))
  }

  return (
    <Container maxWidth="sm">
      <Box mt={5}>
        <Typography variant="h5" gutterBottom>
          高度なフォーム(React Hook Form + MUI)
        </Typography>
        <form onSubmit={handleSubmit(onSubmit)} noValidate>
          <Box display="flex" flexDirection="column" gap={2}>
            <Box>
              <TextField
                fullWidth
                label="名前"
                {...register("name", { required: "名前は必須です" })}
                error={Boolean(errors.name)}
                helperText={errors.name?.message}
              />
            </Box>
            {fields.map((field, index) => (
              <Box key={field.id}>
                <Box display="flex" alignItems="center" gap={1}>
                  <TextField
                    fullWidth
                    label={`メール ${index + 1}`}
                    {...register(`emails.${index}.email`, {
                      required: "メールは必須です",
                      pattern: {
                        value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
                        message: "メール形式が不正です",
                      },
                    })}
                    error={Boolean(errors.emails?.[index]?.email)}
                    helperText={errors.emails?.[index]?.email?.message}
                  />
                  <Button
                    variant="outlined"
                    color="secondary"
                    onClick={() => remove(index)}
                  >
                    <Delete />
                  </Button>
                </Box>
              </Box>
            ))}
            <Box>
              <Button
                variant="outlined"
                startIcon={<Add />}
                onClick={() => append({ email: "" })}
              >
                メール追加
              </Button>
            </Box>
            <Box mt={2} display="flex" justifyContent="center">
              <Button type="submit" variant="contained" color="primary">
                送信
              </Button>
            </Box>
          </Box>
        </form>
      </Box>
    </Container>
  )
}

export default AdvancedForm

4. App.tsxの更新

src/App.tsxを更新してフォームを表示します:

import { ThemeProvider, createTheme } from '@mui/material'
import AdvancedForm from "./AdvancedForm"
import '@fontsource/roboto/300.css'
import '@fontsource/roboto/400.css'
import '@fontsource/roboto/500.css'
import '@fontsource/roboto/700.css'

const theme = createTheme({
  typography: {
    fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
  },
})

function App() {
  return (
    <ThemeProvider theme={theme}>
      <AdvancedForm />
    </ThemeProvider>
  )
}

export default App

🎯 主要なポイント

1. フォーム状態管理

  • useForm: フォーム全体の状態とバリデーション管理
  • useFieldArray: 動的なフィールド配列の管理
  • 型安全な実装: TypeScriptによる型定義

2. バリデーション機能

  • 必須チェック: 名前とメールアドレスの入力必須
  • メール形式: 正規表現によるメールアドレス形式の検証
  • 視覚的フィードバック: エラーメッセージの即時表示

3. UIコンポーネント

  • TextField: 入力フィールドとエラー表示の統合
  • Button: アイコン付きの操作ボタン
  • 動的なUI: フィールドの追加・削除機能

🔚 まとめ

今回は、React Hook Form の useFieldArray を使って、複数メール入力のフォームを実装しました。 動的なフォーム要素の管理とバリデーション処理を効率的に実装できることが分かりました。

📚 次回予告

【第6回】バリデーション結果をモーダルで表示するReact Hook Form実装

2025年5月25日日曜日

【第4回】状態管理とAPI通信(React + Axios)

✅ はじめに

こんにちは!このシリーズでは、React + Vite + TypeScript + MUIを使ってモダンなWebアプリを開発する方法をステップバイステップで解説しています。

第4回となる今回は、「外部APIとの通信(Axios)」と「状態管理(useState, useEffect)」を学びます。APIから取得したデータを画面に表示する実践的な構成です。

📦 使用する技術スタック

ツール バージョン 説明
Axios v1.x HTTP通信を行う人気のライブラリ
MUI v5.x マテリアルデザインベースのUIコンポーネント
TypeScript 5.x 型安全なJavaScript
Vite 5.x 高速な開発環境とビルドツール

🚀 実装手順

1. プロジェクトの作成

まずは、Viteを使って新しいプロジェクトを作成します:

npm create vite@latest react-api-app -- --template react-ts
cd react-api-app
npm install

2. 必要なパッケージのインストール

MUIとAxiosをインストールします:

npm install @mui/material @emotion/react @emotion/styled @fontsource/roboto axios

3. ユーザー一覧コンポーネントの作成

src/pages/UserList.tsxを作成します:

import type { FC } from 'react';
import { useEffect, useState } from 'react';
import type { AxiosResponse } from 'axios';
import axios from 'axios';
import {
  Container,
  Typography,
  List,
  ListItem,
  ListItemText,
  CircularProgress,
  Alert,
  Paper
} from '@mui/material';

type User = {
  id: number;
  name: string;
  email: string;
};

const UserList: FC = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  useEffect(() => {
    axios.get<User[]>('https://jsonplaceholder.typicode.com/users')
      .then((res: AxiosResponse<User[]>) => {
        setUsers(res.data);
        setLoading(false);
      })
      .catch(() => {
        setError('ユーザーの取得に失敗しました。');
        setLoading(false);
      });
  }, []);

  if (loading) {
    return (
      <Container maxWidth="sm">
        <Paper elevation={3} sx={{ p: 4, mt: 4, textAlign: 'center' }}>
          <CircularProgress />
        </Paper>
      </Container>
    );
  }

  if (error) {
    return (
      <Container maxWidth="sm">
        <Paper elevation={3} sx={{ p: 4, mt: 4 }}>
          <Alert severity="error">{error}</Alert>
        </Paper>
      </Container>
    );
  }

  return (
    <Container maxWidth="sm">
      <Paper elevation={3} sx={{ p: 4, mt: 4 }}>
        <Typography variant="h4" component="h1" gutterBottom>
          ユーザー一覧
        </Typography>
        <List>
          {users.map(user => (
            <ListItem key={user.id} divider>
              <ListItemText 
                primary={user.name} 
                secondary={user.email}
              />
            </ListItem>
          ))}
        </List>
      </Paper>
    </Container>
  );
};

export default UserList;

4. App.tsxの更新

最後に、src/App.tsxを更新してユーザー一覧を表示します:

import { ThemeProvider, createTheme } from '@mui/material';
import UserList from './pages/UserList';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';

const theme = createTheme({
  typography: {
    fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
  },
});

function App() {
  return (
    <ThemeProvider theme={theme}>
      <UserList />
    </ThemeProvider>
  );
}

export default App;

🎯 主要なポイント

1. 状態管理

  • useState: データ、ローディング、エラー状態の管理
  • useEffect: コンポーネントマウント時のAPI呼び出し
  • 型安全な状態管理: TypeScriptによる型定義

2. API通信

  • Axios: 型安全なHTTPクライアント
  • エラーハンドリング: try-catchによる例外処理
  • ローディング表示: ユーザー体験の向上

3. UIコンポーネント

  • List/ListItem: データの一覧表示
  • CircularProgress: ローディング表示
  • Alert: エラーメッセージの表示

🔚 まとめ

今回は、Reactで外部APIからデータを取得し、それを状態として管理して表示する方法を学びました。MUIとAxiosを組み合わせることで、実用的な画面が簡単に作成できることが実感できたと思います。

📚 次回予告

【第5回】React Hook Formによるフォーム処理の実践 - 複数メールアドレスの処理

【第3回】フォームの作成とバリデーション(MUI + React Hook Form)

✅ はじめに

こんにちは!このシリーズでは、React + Vite + TypeScript + MUIという現代的な技術スタックを使って、Webアプリを開発する方法をステップバイステップで解説しています。

第3回となる今回は、フォームの作成とバリデーションを扱います。UIには MUI を、バリデーションには React Hook Form を使って、シンプルかつ堅牢なフォームを作成していきましょう。

📦 使用する技術スタック

ツール バージョン 説明
React Hook Form v7.x フォーム管理とバリデーション
MUI v5.x マテリアルデザインベースのUIコンポーネント
TypeScript 5.x 型安全なJavaScript
Vite 5.x 高速な開発環境とビルドツール

🚀 実装手順

1. プロジェクトの作成

まずは、Viteを使って新しいプロジェクトを作成します:

npm create vite@latest my-react-form-app -- --template react-ts
cd my-react-form-app
npm install

2. 必要なパッケージのインストール

MUIとReact Hook Formをインストールします:

npm install @mui/material @emotion/react @emotion/styled @fontsource/roboto react-hook-form

3. フォームコンポーネントの作成

src/pages/UserForm.tsxを作成します:

import type { FC } from 'react';
import { useForm } from 'react-hook-form';
import { TextField, Button, Container, Typography, Paper } from '@mui/material';

type FormData = {
  name: string;
  email: string;
};

const UserForm: FC = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>();

  const onSubmit = (data: FormData): void => {
    alert(`名前: ${data.name}\nメール: ${data.email}`);
  };

  return (
    <Container maxWidth="sm">
      <Paper elevation={3} sx={{ p: 4, mt: 4 }}>
        <Typography variant="h4" component="h1" gutterBottom>
          ユーザーフォーム
        </Typography>
        <form onSubmit={handleSubmit(onSubmit)} noValidate>
          <TextField
            fullWidth
            label="名前"
            margin="normal"
            {...register('name', { required: '名前は必須です' })}
            error={!!errors.name}
            helperText={errors.name?.message}
          />
          <TextField
            fullWidth
            label="メール"
            margin="normal"
            {...register('email', {
              required: 'メールは必須です',
              pattern: {
                value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
                message: '有効なメールアドレスを入力してください',
              },
            })}
            error={!!errors.email}
            helperText={errors.email?.message}
          />
          <Button 
            variant="contained" 
            type="submit" 
            sx={{ mt: 2 }}
            fullWidth
          >
            送信
          </Button>
        </form>
      </Paper>
    </Container>
  );
};

export default UserForm;

4. App.tsxの更新

最後に、src/App.tsxを更新してフォームを表示します:

import { ThemeProvider, createTheme } from '@mui/material';
import UserForm from './pages/UserForm';

const theme = createTheme({
  typography: {
    fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
  },
});

function App() {
  return (
    <ThemeProvider theme={theme}>
      <UserForm />
    </ThemeProvider>
  );
}

export default App;

🎯 主要なポイント

1. React Hook Formの基本概念

  • useForm: フォームの状態管理とバリデーションを提供
  • register: フォームフィールドの登録とバリデーションルールの設定
  • handleSubmit: フォーム送信時の処理を管理
  • formState: エラー状態などのフォーム状態を管理

2. MUIフォームコンポーネント

  • TextField: 入力フィールドのスタイリングとエラー表示
  • Container: レスポンシブなレイアウト
  • Paper: カード風のコンテナスタイリング

3. TypeScriptの型安全性

  • FC型: Reactの関数コンポーネントの型定義
  • FormData型: フォームデータの型定義
  • 明示的な戻り値の型: 関数の戻り値型を指定

🔚 まとめ

今回は、React Hook FormとMUIを組み合わせて、型安全なバリデーション機能付きフォームを実装しました。この組み合わせは実用的で、多くの商用アプリでも採用されています。

📚 次回予告

【第4回】状態管理とAPI通信(React + Axios)

2025年5月24日土曜日

【第2回】React Routerで画面遷移を実装しよう!

✅ はじめに

こんにちは!このシリーズでは、React + Vite + TypeScript + MUIという現代的な技術スタックを使って、Webアプリを開発する方法をステップバイステップで解説していきます。

第2回となる今回は、React Routerを使って「複数のページを持つアプリケーション」を作成します。画面遷移やルーティングの仕組みを理解することで、より本格的なアプリ開発ができるようになります!

📦 使用する技術スタック

ツール バージョン 説明
React Router v6.x Reactアプリケーションのルーティングライブラリ
MUI v5.x マテリアルデザインベースのUIコンポーネント
TypeScript 5.x 型安全なJavaScript
Vite 5.x 高速な開発環境とビルドツール

🚀 実装手順

1. 必要なパッケージのインストール

npm install react-router-dom @mui/material @emotion/react @emotion/styled @fontsource/roboto

2. ページコンポーネントの作成

src/pages/Home.tsx

import { Container, Typography, Paper, Box } from '@mui/material';

const Home = () => {
  return (
    <Container maxWidth="md">
      <Paper elevation={3} sx={{ p: 4, mt: 4 }}>
        <Typography variant="h4" component="h1" gutterBottom>
          ホームページ
        </Typography>
        <Box sx={{ mt: 2 }}>
          <Typography variant="body1">
            このアプリケーションは、React Router v6を使用した
            シンプルなルーティングのデモンストレーションです。
          </Typography>
        </Box>
      </Paper>
    </Container>
  );
};

export default Home;

src/pages/About.tsx

import { Container, Typography, Paper, Box, List, ListItem, ListItemText } from '@mui/material';

const About = () => {
  return (
    <Container maxWidth="md">
      <Paper elevation={3} sx={{ p: 4, mt: 4 }}>
        <Typography variant="h4" component="h1" gutterBottom>
          このアプリについて
        </Typography>
        <Box sx={{ mt: 2 }}>
          <Typography variant="body1" paragraph>
            このアプリケーションは以下の技術スタックを使用しています:
          </Typography>
          <List>
            <ListItem>
              <ListItemText 
                primary="React + TypeScript" 
                secondary="型安全なコンポーネント開発"
              />
            </ListItem>
            <ListItem>
              <ListItemText 
                primary="React Router v6" 
                secondary="モダンなルーティング機能"
              />
            </ListItem>
            <ListItem>
              <ListItemText 
                primary="Material-UI (MUI)" 
                secondary="美しいUIコンポーネント"
              />
            </ListItem>
          </List>
        </Box>
      </Paper>
    </Container>
  );
};

export default About;

3. App.tsxでのルーティング設定

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import { 
  AppBar, 
  Toolbar, 
  Button, 
  Container, 
  ThemeProvider, 
  createTheme,
  Box,
  Typography
} from '@mui/material';
import Home from './pages/Home';
import About from './pages/About';

const theme = createTheme({
  typography: {
    fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
  },
});

function App() {
  return (
    <ThemeProvider theme={theme}>
      <BrowserRouter>
        <Box sx={{ flexGrow: 1 }}>
          <AppBar position="static">
            <Toolbar>
              <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
                React Router Demo
              </Typography>
              <Button 
                color="inherit" 
                component={Link} 
                to="/"
                sx={{ mr: 2 }}
              >
                Home
              </Button>
              <Button 
                color="inherit" 
                component={Link} 
                to="/about"
              >
                About
              </Button>
            </Toolbar>
          </AppBar>
        </Box>

        <Container>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
          </Routes>
        </Container>
      </BrowserRouter>
    </ThemeProvider>
  );
}

export default App;

🎯 主要なポイント

1. React Routerの基本概念

  • BrowserRouter: URLベースのルーティングを提供
  • Routes: ルート定義のコンテナ
  • Route: 個別のルートを定義
  • Link: ページ間のナビゲーション

2. MUIとの統合

  • AppBarによるナビゲーションバー
  • Containerによるレイアウト
  • Buttonコンポーネントとリンクの統合

3. 型安全性

  • TypeScriptによる型チェック
  • コンポーネントpropsの型定義

🔚 まとめ

このチュートリアルでは、React RouterとMUIを組み合わせて、モダンなナビゲーションを持つSPAを構築する方法を学びました。次回は、フォームの実装とバリデーションについて解説します。

📚 次回予告

【第3回】フォームの作成とバリデーション(MUI + React Hook Form)

【第1回】React + Vite + TypeScript + MUIで開発環境を最速構築しよう【2025年版】

✅はじめに

こんにちは!このシリーズでは、React + Vite + TypeScript + MUIという現代的な技術スタックを使って、Webアプリを開発する方法をステップバイステップで解説していきます。

第1回となる今回は、開発環境の構築と初期プロジェクトの作成、そしてMUIを使った簡単なUI表示までを行います。

📦使用する技術スタック

ツール バージョン 説明
React 19.1.0 フロントエンドJavaScriptライブラリ
Vite 6.3.5 爆速な開発環境を提供するビルドツール
TypeScript 5.8.3 型安全なJavaScriptのスーパーセット
MUI 7.1.0 Googleのマテリアルデザインに基づいたUIライブラリ

🛠1.必要なツールの準備

以下がインストールされていることを確認してください。

  • Node.js(最新版)
  • 任意のエディタ(例:Visual Studio Code)

🚀2.プロジェクトの作成

以下のコマンドをターミナルで実行して、Vite + React + TypeScript のプロジェクトを作成します。

npm create vite@latest my-react-app -- --template react-ts
cd my-react-app
npm install

🎨3.MUIのインストール

次に、MUIのライブラリとRobotoフォントをインストールします。

npm install @mui/material @emotion/react @emotion/styled
npm install @fontsource/roboto

🧹4.index.cssを空にする

Vite のテンプレートには、初期状態でスタイルが含まれています。このスタイルは MUI のレイアウトに干渉するため、src/index.css の中身をすべて削除(もしくはコメントアウト)してください。

🧑‍💻5.main.tsxの編集

Robotoフォントを適用するために、src/main.tsxを以下のように編集します:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

// Robotoフォントのインポート
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

👨‍💻6.App.tsxの作成

次に、src/App.tsxを以下のように書き換えて、MUIのコンポーネントとRobotoフォントを使用したUIを作成します:

import { useState } from 'react'
import { Button, Container, Typography, ThemeProvider, createTheme, Box } from '@mui/material'

type CounterProps = {
  initialCount?: number;
};

// カスタムテーマを作成
const theme = createTheme({
  typography: {
    fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
  },
});

function App({ initialCount = 0 }: CounterProps) {
  const [count, setCount] = useState(initialCount)

  return (
    <ThemeProvider theme={theme}>
      <Container maxWidth="sm" sx={{ textAlign: 'center', mt: 5 }}>
        <Typography variant="h4" gutterBottom>
          React + Vite + MUI スターター
        </Typography>
        <Typography variant="body1" gutterBottom>
          カウント: {count}
        </Typography>
        <Button variant="contained" onClick={() => setCount(count + 1)}>
          カウントを増やす
        </Button>

        {/* フォントの比較表示 */}
        <Box sx={{ mt: 4, display: 'flex', flexDirection: 'column', gap: 2 }}>
          <Typography variant="h6">
            フォントの比較
          </Typography>
          <Typography sx={{ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif' }}>
            これはRobotoフォントです (The quick brown fox jumps over the lazy dog)
          </Typography>
          <Typography sx={{ fontFamily: 'system-ui, -apple-system, sans-serif' }}>
            これはシステムフォントです (The quick brown fox jumps over the lazy dog)
          </Typography>
        </Box>
      </Container>
    </ThemeProvider>
  )
}

export default App

✨主な変更点と改善点

  1. Robotoフォントの追加: MUIのデフォルトフォントであるRobotoを正しく実装
  2. ThemeProviderの導入: フォントファミリーを明示的に設定し、一貫したデザインを実現
  3. TypeScriptの活用: CounterPropsインターフェースを定義し、型安全性を確保
  4. フォント比較機能: Robotoフォントとシステムフォントの違いを視覚的に確認可能

✅表示確認

以下のコマンドで開発サーバーを起動しましょう。

npm run dev

ブラウザでhttp://localhost:5173を開くと、以下の要素が確認できます:

  • Robotoフォントを使用したヘッダーテキスト
  • カウンター機能付きのボタン
  • フォントの違いを確認できる比較セクション

🔚まとめ

これで、React + Vite + TypeScript + MUI の環境が整い、Robotoフォントも正しく実装されました!

📌次回予告

次回は、この環境を使って複数ページを作成する方法(React Router)を紹介します。お楽しみに!