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:フォームの状態管理とコンテキスト