ReactとJavaScriptで問い合わせフォームの作成してみた！その4

# はじめに
こんにちは！WEBエンジニア転職を目指しているK.Yです！
今回は、お問い合わせフォームを実装してみました！
以下、完成したお問い合わせフォームです！

![fuga.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3719491/0ba1906f-74cd-2efb-e858-39286dd0c2cd.gif)

ブログ作成の過程を以下で、それぞれ纏めましたので、
良かったら見てください:sunglasses:

https://qiita.com/ken887265/items/a6dfc1f62ee185b986b2

https://qiita.com/ken887265/items/8d6255a5aba95897ca67

https://qiita.com/ken887265/items/f8850ec0fd5757c2bd39

:::note warn
重複している部分は省略しております！
:::

## 対象者
・　React初心者
・　フロントエンドに興味がある人

## バージョン
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1"

JavaScript　ES6以降

## コード

```javascript
PostsList.js

import React, { useEffect, useState } from 'react'
import './App.css'
import { Link } from 'react-router-dom';
const PostsList = () => {

  const formatDate = (dateString) => {
    const date = new Date(dateString);
    const options = { year: 'numeric', month: 'numeric', day: 'numeric' };

    return date.toLocaleDateString('ja-JP', options);
  };
  const [posts, setPosts] = useState({ articles: [] });
  const [loading, setLoading] = useState(true); 
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch("https://1hmfpsvto6.execute-api.ap-northeast-1.amazonaws.com/dev/posts");
        const data = await response.json();
        
        setPosts( data );
      }  finally {
        setLoading(false); 
      }
    };
    fetchData();
  }, []);
  if (loading) {
    return <div>Loading...</div>; 
  }
  return (
    <div className="App">
      <header className="header-App">
        <Link className="link" to="/">Blog</Link>
        <Link className="link" to="/inquiry">お問い合わせ</Link>
      </header>

      {
        Array.isArray(posts.posts) && posts.posts.map(article => (
          <div key={article.id} className="posts-info">
            <ul >
              <li>
                <Link to={`/post/${article.id}`}>
                  <div className="date">{formatDate(article.createdAt)}</div>
                  <div className="programming-language">{article.categories.map((category, idx) => (
                    <span key={idx} className="category-box">{category}</span>
                  ))}</div>
                  <div className="title">{article.title}</div>
                  <div className="content" dangerouslySetInnerHTML={{ __html: article.content }}>
                  </div>
                </Link>
              </li>
            </ul>
          </div>
        ))}
    </div>
  );
}
export default PostsList;
```

```javascript
DetailsPage.js

mport { Link, useParams } from 'react-router-dom';
import React, { useEffect, useState } from 'react'
import './App.css';
const DetailsPage = () => {
  const { id } = useParams();
  const formatDate = (dateString) => {
    const date = new Date(dateString);
    const options = { year: 'numeric', month: 'numeric', day: 'numeric' };
    return date.toLocaleDateString('ja-JP', options);
  }
  const [detailsData, setDetailsData] = useState();
  const [loading, setLoading] = useState(true); 
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(`https://1hmfpsvto6.execute-api.ap-northeast-1.amazonaws.com/dev/posts/${id}`);
        const result = await response.json();
        setDetailsData(result.post); 
      } finally {
        setLoading(false); 
      }
    };
    fetchData();
  }, [id]);
  if (loading) {
    return <div>Loading...</div>; 
  }
  if (!detailsData) return <div>投稿が見つかりません</div>;
  return (
    <div className='App'>
      <header className="header-App">
        <Link to="/" className="link">Blog</Link>
        <Link to="/inquiry" className="link" >お問い合わせ</Link>
      </header>

        <div style={{ border: 'none' }} className="posts-info">
          <ul>
            <li key={detailsData.id}>
              <div><img src={detailsData.thumbnailUrl} alt="img" /></div>
              <div className="date">{formatDate(detailsData.createdAt)}</div>
              <div className="programming-language">
                {detailsData.categories.map((category, idx) => (
                  <span key={idx} className="category-box">{category}</span>
                ))}
              </div>
              <div className="title">{detailsData.title}</div>
              <div style={{ display: 'block' }} className="content" dangerouslySetInnerHTML={{ __html: detailsData.content }}></div>
            </li>
          </ul>
        </div>
    </div>
  );
}
export default DetailsPage;
```

```javascript
InquiryPage.js


import { Link } from 'react-router-dom';
import React, { useState } from 'react';
import './App.css';
const InquiryPage = () => {
  const [inquiryData, setInquiryData] = useState({ name: '', email: '', message: '' });
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const handleChange = (e) => {
    const { id, value } = e.target;
    setInquiryData((prevData) => ({ ...prevData, [id]: value }));
  };
  const validate = () => {
    const tempErrors = {};
    if (!inquiryData.name) tempErrors.name = 'お名前は必須です。';
    if (!inquiryData.email) tempErrors.email = 'メールアドレスは必須です。';
    if (!inquiryData.message) tempErrors.message = '本文は必須です。';
    setErrors(tempErrors);
    return Object.keys(tempErrors).length === 0;
  };
  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!validate()) return;
    setIsSubmitting(true);
    try {
      const response = await fetch("https://1hmfpsvto6.execute-api.ap-northeast-1.amazonaws.com/dev/contacts", {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(inquiryData),
      });
      if (!response.ok) throw new Error('Network response was not ok');
      alert('送信しました');
      setInquiryData({ name: '', email: '', message: '' });
      setErrors({});
    } catch (error) {
      console.error('Error submitting form:', error);
    } finally {
      setIsSubmitting(false);
    }
  };

  const handleClear = () => {
    setInquiryData({name: '', email: '', message: ''});
  }
  return (
    <div className='App'>
      <header className="header-App">
        <Link to="/" className="link">Blog</Link>
        <Link to="/inquiry" className="link">お問い合わせ</Link>
      </header>
      <div className="inquiry">
        <h1>問合わせフォーム</h1>
        <form id="myForm" onSubmit={handleSubmit}>
          <div className="formItem">
              <label>
              <dl>
                <dt>お名前</dt>
                <div className="text">
                  <dd>
                    <input
                    type="text"
                    id="name"
                    maxLength="30"
                    value={inquiryData.name}
                    onChange={handleChange}
                    disabled={isSubmitting}
                  />
                  </dd>
                  {errors.name && <span>{errors.name}</span>}
                </div>
                </dl>
                </label>
                
            <div className="label">
              <label>
                <dl>
                <dt>メールアドレス</dt>
                <div className="text">
                  <dd>
                    <input
                    type="text"
                    id="email"
                    value={inquiryData.email}
                    onChange={handleChange}
                    disabled={isSubmitting}
                  />
                  </dd>
                  {errors.email && <span>{errors.email}</span>}
                </div>
                </dl>
                </label>
            </div>
            <div className="label">
              <label>
                <dl>
                  <dt>本文</dt>
              <div className="text">
              <dd>
                <textarea
                  type="text"
                  id="message"
                  maxLength="500"
                  value={inquiryData.message}
                  onChange={handleChange}
                  disabled={isSubmitting}
                  rows="10"
                />
                </dd>
                {errors.message && <span>{errors.message}</span>}
              </div>
              </dl>
              </label>
            </div>
          </div>
          <div className="btn">
          <input type="submit" value="送信" disabled={isSubmitting} />
          <input type="reset" value="クリア" onClick={handleClear} disabled={isSubmitting} />
          </div>
        </form>
      </div>
    </div>
  );
};
export default InquiryPage;
```

```javascript
App.js

import React from 'react';
import './App.css';
import { Routes, Route } from 'react-router-dom';
import PostsList from './PostsList';
import DetailsPage from './DetailsPage';
import InquiryPage from './InquiryPage';
const App = () => {
  return (
    <Routes>
      <Route path="/" element={<PostsList />} />
      <Route path="/post/:id" element={<DetailsPage  />} />
      <Route path="/inquiry" element={<InquiryPage />} />
    </Routes>
  );
```
## InquiryPage.js (お問い合わせフォーム)

```javascript
const [inquiryData, setInquiryData] = useState({ name: '', email: '', message: '' });
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
```

・`inquiryData`と`setInquiryData`は、フォームの入力データを管理するためのステート。
 `useState`の引数として渡されるオブジェクト`{ name: '', email: '', message: '' }`は、ステートの初期値です。
 それぞれ空文字列で設定。


・`errors`と`setErrors`は、フォームのバリデーションエラーメッセージを管理するためのステートです。

## 入力変更ハンドラー
```javascript
const handleChange = (e) => {
  const { id, value } = e.target;
  setInquiryData((prevData) => ({ ...prevData, [id]: value }));
};
```
・`handleChange`関数は、フォームの入力が変更されたときに呼び出されます。

・`e.target.id`は変更された入力フィールドのIDを取得します。

・`e.target.value`は変更された入力フィールドの値を取得します。

・`setInquiryData`を使って、`inquiryData`ステートを更新します。

・スプレッド構文`...`を使って、`prevData`オブジェクトの全てのプロパティを新しいオブジェクトにコピーします。
これにより、既存のステートを保持しつつ、一部のプロパティだけを更新することができます。

## バリデーション関数
```javascript
const validate = () => {
  const tempErrors = {};
  if (!inquiryData.name) tempErrors.name = 'お名前は必須です。';
  if (!inquiryData.email) tempErrors.email = 'メールアドレスは必須です。';
  if (!inquiryData.message) tempErrors.message = '本文は必須です。';
  setErrors(tempErrors);
  return Object.keys(tempErrors).length === 0;
};
```
・`validate`関数は、フォームの入力データを検証します。

・`inquiryData`の各フィールドが空である場合、エラーメッセージを`tempErrors`オブジェクトに追加。

・`setErrors`を使って、`errors`ステートを更新します。

・`tempErrors`オブジェクトが空であれば`true`を返し、そうでなければ`false`を返す。

## フォーム送信ハンドラー
```javascript
const handleSubmit = async (e) => {
  e.preventDefault();
  if (!validate()) return;
  setIsSubmitting(true);
  try {
    const response = await fetch("https://1hmfpsvto6.execute-api.ap-northeast-1.amazonaws.com/dev/contacts", {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(inquiryData),
    });
    if (!response.ok) throw new Error('Network response was not ok');
    alert('送信しました');
    setInquiryData({ name: '', email: '', message: '' });
    setErrors({});
  } catch (error) {
    console.error('Error submitting form:', error);
  } finally {
    setIsSubmitting(false);
  }
};
```
・`handleSubmit`関数は、フォームが送信されたときに呼び出される。

・`e.preventDefault()`を使って、フォームのデフォルトの送信動作を防ぐ。

・`validate`関数を呼び出して、入力データが有効でない場合は処理を中断。

・`setIsSubmitting(true)`を使って、フォームが送信中であることを示す。

・`fetch`関数を使って、APIにデータを送信します。

・`method:` `POST`はHTTPメソッドをPOSTに設定します。

・`headers: { 'Content-Type': 'application/json' }`はリクエストヘッダーを設定します。

・`body: JSON.stringify(inquiryData)`はリクエストボディにJSON形式のデータを設定します。

・`setErrors`は、フォームのバリデーションエラーを管理するためのステート更新関数です。

・`setErrors({})`は、エラーオブジェクトを空にリセットします。これにより、前回の送信時に発生したエラーメッセージがクリアされます。

・　`catch`ブロックは、`try`ブロック内で発生したエラーをキャッチして処理。
  `catch (error)`の`error`は、発生したエラーオブジェクトを指します。
  `console.error('Error submitting form:', error)`は、エラーメッセージをコンソールに出    力します。これにより、デバッグやログの確認が容易になります。

・`finally`ブロックは、`try`ブロックが成功しても失敗しても必ず実行されます。
  finallyブロック内で行われる処理は、リソースの解放やクリーンアップ操作などです。
  setIsSubmitting(false)は、フォームの送信状態を解除します。これにより、送信ボタンが再び有効になります。

## フォームクリアハンドラー

```javascript
const handleClear = () => {
  setInquiryData({ name: '', email: '', message: '' });
};
```
・`handleClear関数`は、フォームの各入力データをクリアします。

## JSXのレンダリング
```javascript
return (
  <div className='App'>
    <header className="header-App">
      <Link to="/" className="link">Blog</Link>
      <Link to="/inquiry" className="link">お問い合わせ</Link>
    </header>
    <div className="inquiry">
      <h1>問合わせフォーム</h1>
      <form id="myForm" onSubmit={handleSubmit}>
        <div className="formItem">
          <label>
            <dl>
              <dt>お名前</dt>
              <div className="text">
                <dd>
                  <input
                    type="text"
                    id="name"
                    maxLength="30"
                    value={inquiryData.name}
                    onChange={handleChange}
                    disabled={isSubmitting}
                  />
                </dd>
                {errors.name && <span>{errors.name}</span>}
              </div>
            </dl>
          </label>
          <div className="label">
            <label>
              <dl>
                <dt>メールアドレス</dt>
                <div className="text">
                  <dd>
                    <input
                      type="text"
                      id="email"
                      value={inquiryData.email}
                      onChange={handleChange}
                      disabled={isSubmitting}
                    />
                  </dd>
                  {errors.email && <span>{errors.email}</span>}
                </div>
              </dl>
            </label>
          </div>
          <div className="label">
            <label>
              <dl>
                <dt>本文</dt>
                <div className="text">
                  <dd>
                    <textarea
                      type="text"
                      id="message"
                      maxLength="500"
                      value={inquiryData.message}
                      onChange={handleChange}
                      disabled={isSubmitting}
                      rows="10"
                    />
                  </dd>
                  {errors.message && <span>{errors.message}</span>}
                </div>
              </dl>
            </label>
          </div>
        </div>
        <div className="btn">
          <input type="submit" value="送信" disabled={isSubmitting} />
          <input type="reset" value="クリア" onClick={handleClear} disabled={isSubmitting} />
        </div>
      </form>
    </div>
  </div>
);
```
## フォームの定義
```javascript
<form id="myForm" onSubmit={handleSubmit}>
  ...
</form>
```
・`<form>`タグは、フォーム全体を定義。
  `id="myForm"`はフォームのIDを設定します。
  `onSubmit={handleSubmit}`は、フォームが送信されたときに`handleSubmit`関数を呼び出す。

## フォームの各フィールド(お名前フィールド)
```javascript
<div className="formItem">
  <label>
    <dl>
      <dt>お名前</dt>
      <div className="text">
        <dd>
          <input
            type="text"
            id="name"
            maxLength="30"
            value={inquiryData.name}
            onChange={handleChange}
            disabled={isSubmitting}
          />
        </dd>
        {errors.name && <span>{errors.name}</span>}
      </div>
    </dl>
  </label>
</div>
```
・`<label>`タグは、フォームフィールドのラベルを定義

・`<dl>`タグは、定義リストを作成。

・`<dt>`タグは用語（この場合は「お名前」）を定義。

・`<div className="text">`は、入力フィールドを囲むコンテナです。

・`<dd>`タグは定義の説明部分を定義。

・`<input>`タグは、テキスト入力フィールドを定義。

・`type="text"`は、テキスト入力フィールドであることを示します。

・`id="name"`は、フィールドのIDを設定。

・`maxLength="30"`は、入力できる文字数の最大値を設定。

・`value={inquiryData.name}`は、フィールドの値を`inquiryData.name`にバインドします。

・`onChange={handleChange}`は、入力が変更されたときに`handleChange`関数を呼び出す。

・`disabled={isSubmitting}`は、フォームが送信中の場合にフィールドを無効化。

・`{errors.name && <span>{errors.name}</span>}`は、`errors.name`が存在する場合にエラーメッセージを表示。

## メールアドレスフィールド
```javascript
<div className="label">
  <label>
    <dl>
      <dt>メールアドレス</dt>
      <div className="text">
        <dd>
          <input
            type="text"
            id="email"
            value={inquiryData.email}
            onChange={handleChange}
            disabled={isSubmitting}
          />
        </dd>
        {errors.email && <span>{errors.email}</span>}
      </div>
    </dl>
  </label>
</div>
```
・メールアドレスフィールドは、お名前フィールドと同様の構造を持ちますが、`id`と`value`が`email`に変更されています。

## 本文フィールド
```javascript
<div className="label">
  <label>
    <dl>
      <dt>本文</dt>
      <div className="text">
        <dd>
          <textarea
            type="text"
            id="message"
            maxLength="500"
            value={inquiryData.message}
            onChange={handleChange}
            disabled={isSubmitting}
            rows="10"
          />
        </dd>
        {errors.message && <span>{errors.message}</span>}
      </div>
    </dl>
  </label>
</div>
```
・本文フィールドも同様の構造を持ちますが、`<textarea>`タグを使用して複数行のテキスト入力を可能にしています。
 `rows="10"`は、テキストエリアの行数を設定します。

 ## 送信ボタンとクリアボタン
 ```javascript
 <div className="btn">
  <input type="submit" value="送信" disabled={isSubmitting} />
  <input type="reset" value="クリア" onClick={handleClear} disabled={isSubmitting} />
</div>
 ```
 ・`<input type="submit">`は、フォームを送信するボタンです。
 
 ・`value="送信"`は、ボタンの表示テキストを設定。

 ・`disabled={isSubmitting}`は、フォームが送信中の場合にボタンを無効化します。

 ・`<input type="reset">`は、フォームをリセットするボタンです。
 
・`value="クリア"`は、ボタンの表示テキストを設定します。

・`onClick={handleClear}`は、ボタンがクリックされたときに`handleClear`関数を呼び出す。

・`disabled={isSubmitting}`は、フォームが送信中の場合にボタンを無効化します。

## ポイント
・`イベントハンドリング`
Reactでは、イベントハンドラーを使ってユーザーの入力や操作に応じた処理を行います。
例) 項目: 入力変更ハンドラー・フォーム送信ハンドラー・フォームクリアハンドラー

・`フォームのバリデーション`
**入力の検証**：入力データを検証し、エラーメッセージを表示。
例) 項目: バリデーション関数

## おわり
今回は、お問い合わせフォームを実装してみました！
React/JavaScriptの基礎を勉強して、アウトプット用に簡易的なブログ作ってみました！
まだまだ理解は浅いですが、コツコツと頑張っていきます！
今回の記事が少しでもお役に立てたら嬉しいです！