はじめに
こんにちは!WEBエンジニア転職を目指しているK.Yです!
今回は、お問い合わせフォームを実装してみました!
以下、完成したお問い合わせフォームです!
ブログ作成の過程を以下で、それぞれ纏めましたので、
良かったら見てください
重複している部分は省略しております!
対象者
・ React初心者
・ フロントエンドに興味がある人
バージョン
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1"
JavaScript ES6以降
コード
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;
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;
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;
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 (お問い合わせフォーム)
const [inquiryData, setInquiryData] = useState({ name: '', email: '', message: '' });
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
・inquiryData
とsetInquiryData
は、フォームの入力データを管理するためのステート。
useState
の引数として渡されるオブジェクト{ name: '', email: '', message: '' }
は、ステートの初期値です。
それぞれ空文字列で設定。
・errors
とsetErrors
は、フォームのバリデーションエラーメッセージを管理するためのステートです。
入力変更ハンドラー
const handleChange = (e) => {
const { id, value } = e.target;
setInquiryData((prevData) => ({ ...prevData, [id]: value }));
};
・handleChange
関数は、フォームの入力が変更されたときに呼び出されます。
・e.target.id
は変更された入力フィールドのIDを取得します。
・e.target.value
は変更された入力フィールドの値を取得します。
・setInquiryData
を使って、inquiryData
ステートを更新します。
・スプレッド構文...
を使って、prevData
オブジェクトの全てのプロパティを新しいオブジェクトにコピーします。
これにより、既存のステートを保持しつつ、一部のプロパティだけを更新することができます。
バリデーション関数
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
を返す。
フォーム送信ハンドラー
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)は、フォームの送信状態を解除します。これにより、送信ボタンが再び有効になります。
フォームクリアハンドラー
const handleClear = () => {
setInquiryData({ name: '', email: '', message: '' });
};
・handleClear関数
は、フォームの各入力データをクリアします。
JSXのレンダリング
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>
);
フォームの定義
<form id="myForm" onSubmit={handleSubmit}>
...
</form>
・<form>
タグは、フォーム全体を定義。
id="myForm"
はフォームのIDを設定します。
onSubmit={handleSubmit}
は、フォームが送信されたときに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>
・<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
が存在する場合にエラーメッセージを表示。
メールアドレスフィールド
<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
に変更されています。
本文フィールド
<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"
は、テキストエリアの行数を設定します。
送信ボタンとクリアボタン
<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の基礎を勉強して、アウトプット用に簡易的なブログ作ってみました!
まだまだ理解は浅いですが、コツコツと頑張っていきます!
今回の記事が少しでもお役に立てたら嬉しいです!