
Reactをセットアップするための環境設定
はじめに React(リアクト)は、Facebook(現Meta)が開発したJavaScriptライブラリで、主にWebアプリケーションの**ユーザーインターフ […]
Reactにおけるフォーム処理は、HTMLフォームとは異なるアプローチが必要です。この章では、Reactの「Controlled Components(制御されたコンポーネント)」という概念を中心に、フォーム処理の基本から実践的な使い方までを詳細に解説します。
Reactのフォーム処理には2つのアプローチがあります:
Controlled Componentsでは、以下のサイクルで動作します:
import { useState } from 'react';
function ControlledForm() {
const [value, setValue] = useState('');
const handleChange = (event) => {
setValue(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
alert(`入力値: ${value}`);
};
return (
<form onSubmit={handleSubmit}>
<label>
名前:
<input type="text" value={value} onChange={handleChange} />
</label>
<button type="submit">送信</button>
</form>
);
}
function TextInput() {
const [text, setText] = useState('');
return (
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="テキストを入力"
/>
);
}
function TextAreaInput() {
const [text, setText] = useState('');
return (
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="複数行のテキストを入力"
/>
);
}
function SelectBox() {
const [selectedOption, setSelectedOption] = useState('option1');
return (
<select
value={selectedOption}
onChange={(e) => setSelectedOption(e.target.value)}
>
<option value="option1">オプション1</option>
<option value="option2">オプション2</option>
<option value="option3">オプション3</option>
</select>
);
}
function CheckboxExample() {
const [isChecked, setIsChecked] = useState(false);
return (
<label>
<input
type="checkbox"
checked={isChecked}
onChange={(e) => setIsChecked(e.target.checked)}
/>
同意する
</label>
);
}
function RadioButtons() {
const [selectedOption, setSelectedOption] = useState('option1');
return (
<div>
<label>
<input
type="radio"
value="option1"
checked={selectedOption === 'option1'}
onChange={(e) => setSelectedOption(e.target.value)}
/>
オプション1
</label>
<label>
<input
type="radio"
value="option2"
checked={selectedOption === 'option2'}
onChange={(e) => setSelectedOption(e.target.value)}
/>
オプション2
</label>
</div>
);
}
function MultiInputForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log({ name, email, age });
};
return (
<form onSubmit={handleSubmit}>
<label>
名前:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<label>
メールアドレス:
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</label>
<label>
年齢:
<input
type="number"
value={age}
onChange={(e) => setAge(e.target.value)}
/>
</label>
<button type="submit">送信</button>
</form>
);
}
function UnifiedStateForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
age: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(formData);
};
return (
<form onSubmit={handleSubmit}>
<label>
名前:
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
/>
</label>
<label>
メールアドレス:
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</label>
<label>
年齢:
<input
type="number"
name="age"
value={formData.age}
onChange={handleChange}
/>
</label>
<button type="submit">送信</button>
</form>
);
}
function ValidatedForm() {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const validate = () => {
const newErrors = {};
if (!formData.email) {
newErrors.email = 'メールアドレスは必須です';
} else if (!/^\S+@\S+\.\S+$/.test(formData.email)) {
newErrors.email = '有効なメールアドレスを入力してください';
}
if (!formData.password) {
newErrors.password = 'パスワードは必須です';
} else if (formData.password.length < 8) {
newErrors.password = 'パスワードは8文字以上必要です';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
console.log('フォームが送信されました:', formData);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>メールアドレス:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
{errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}
</div>
<div>
<label>パスワード:</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
/>
{errors.password && <span style={{ color: 'red' }}>{errors.password}</span>}
</div>
<button type="submit">ログイン</button>
</form>
);
}
function RealTimeValidation() {
const [formData, setFormData] = useState({
username: '',
email: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validateField = (name, value) => {
let error = '';
if (name === 'username') {
if (!value) error = 'ユーザー名は必須です';
else if (value.length < 3) error = '3文字以上必要です';
}
if (name === 'email') {
if (!value) error = 'メールアドレスは必須です';
else if (!/^\S+@\S+\.\S+$/.test(value)) error = '有効なメールアドレスを入力してください';
}
return error;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
if (touched[name]) {
setErrors(prev => ({
...prev,
[name]: validateField(name, value)
}));
}
};
const handleBlur = (e) => {
const { name, value } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
setErrors(prev => ({
...prev,
[name]: validateField(name, value)
}));
};
const handleSubmit = (e) => {
e.preventDefault();
// 全てのフィールドを検証
const newErrors = {};
Object.keys(formData).forEach(name => {
newErrors[name] = validateField(name, formData[name]);
});
setErrors(newErrors);
// エラーがない場合のみ送信
if (Object.values(newErrors).every(error => !error)) {
console.log('フォームが送信されました:', formData);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>ユーザー名:</label>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
onBlur={handleBlur}
/>
{errors.username && <span style={{ color: 'red' }}>{errors.username}</span>}
</div>
<div>
<label>メールアドレス:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
/>
{errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}
</div>
<button type="submit">登録</button>
</form>
);
}
複数のフォームで同じロジックを再利用するために、カスタムフックを作成できます。
function useForm(initialValues, validate) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({
...prev,
[name]: value
}));
if (touched[name]) {
setErrors(prev => ({
...prev,
[name]: validate[name](value)
}));
}
};
const handleBlur = (e) => {
const { name, value } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
setErrors(prev => ({
...prev,
[name]: validate[name](value)
}));
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
setValues,
setErrors,
setTouched
};
}
// 使用例
function UserForm() {
const { values, errors, touched, handleChange, handleBlur } = useForm(
{ username: '', email: '' },
{
username: (value) => {
if (!value) return 'ユーザー名は必須です';
if (value.length < 3) return '3文字以上必要です';
return '';
},
email: (value) => {
if (!value) return 'メールアドレスは必須です';
if (!/^\S+@\S+\.\S+$/.test(value)) return '有効なメールアドレスを入力してください';
return '';
}
}
);
const handleSubmit = (e) => {
e.preventDefault();
// フォーム送信処理
};
return (
<form onSubmit={handleSubmit}>
{/* フォームフィールド */}
</form>
);
}
大きなフォームでは、各入力ごとにコンポーネント全体が再レンダリングされるのを防ぐために、個々の入力フィールドをメモ化できます。
const MemoizedInput = React.memo(function Input({ label, name, value, error, touched, onChange, onBlur }) {
return (
<div>
<label>{label}</label>
<input
type="text"
name={name}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
{touched && error && <span style={{ color: 'red' }}>{error}</span>}
</div>
);
});
function OptimizedForm() {
// ...フォームロジック...
return (
<form onSubmit={handleSubmit}>
<MemoizedInput
label="ユーザー名"
name="username"
value={values.username}
error={errors.username}
touched={touched.username}
onChange={handleChange}
onBlur={handleBlur}
/>
{/* 他のフィールド */}
</form>
);
}
大規模なアプリケーションでは、フォーム処理ライブラリを使用すると便利です。
import { Formik, Form, Field, ErrorMessage } from 'formik';
function FormikForm() {
return (
<Formik
initialValues={{ email: '', password: '' }}
validate={values => {
const errors = {};
if (!values.email) {
errors.email = '必須項目です';
} else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
) {
errors.email = '無効なメールアドレスです';
}
return errors;
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 400);
}}
>
{({ isSubmitting }) => (
<Form>
<label htmlFor="email">メールアドレス</label>
<Field type="email" name="email" />
<ErrorMessage name="email" component="div" />
<label htmlFor="password">パスワード</label>
<Field type="password" name="password" />
<ErrorMessage name="password" component="div" />
<button type="submit" disabled={isSubmitting}>
送信
</button>
</Form>
)}
</Formik>
);
}
import { useForm } from 'react-hook-form';
function HookForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>ユーザー名</label>
<input
{...register("username", { required: true, minLength: 3 })}
/>
{errors.username?.type === "required" && (
<p>ユーザー名は必須です</p>
)}
{errors.username?.type === "minLength" && (
<p>3文字以上入力してください</p>
)}
<label>メールアドレス</label>
<input
type="email"
{...register("email", { required: true, pattern: /^\\S+@\\S+\\.\\S+$/ })}
/>
{errors.email && <p>有効なメールアドレスを入力してください</p>}
<button type="submit">登録</button>
</form>
);
}
問題:大きなフォームで入力が遅れる
解決策:
function DebouncedInput({ value, onChange, ...props }) {
const [internalValue, setInternalValue] = useState(value);
const timerRef = useRef();
useEffect(() => {
setInternalValue(value);
}, [value]);
const handleChange = (e) => {
const newValue = e.target.value;
setInternalValue(newValue);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
onChange(newValue);
}, 300);
};
return <input {...props} value={internalValue} onChange={handleChange} />;
}
問題:複数のフィールドに依存するバリデーション
解決策:フォーム全体のバリデーション関数を使用
function validateForm(values) {
const errors = {};
if (!values.password) {
errors.password = 'パスワードは必須です';
}
if (!values.confirmPassword) {
errors.confirmPassword = '確認用パスワードは必須です';
} else if (values.password !== values.confirmPassword) {
errors.confirmPassword = 'パスワードが一致しません';
}
return errors;
}
問題:フィールド数が動的に変化する
解決策:配列ベースのstate管理
function DynamicForm() {
const [fields, setFields] = useState([{ id: 1, value: '' }]);
const addField = () => {
setFields([...fields, { id: Date.now(), value: '' }]);
};
const removeField = (id) => {
setFields(fields.filter(field => field.id !== id));
};
const handleChange = (id, value) => {
setFields(fields.map(field =>
field.id === id ? { ...field, value } : field
));
};
return (
<div>
{fields.map(field => (
<div key={field.id}>
<input
value={field.value}
onChange={(e) => handleChange(field.id, e.target.value)}
/>
<button onClick={() => removeField(field.id)}>削除</button>
</div>
))}
<button onClick={addField}>フィールドを追加</button>
</div>
);
}
Reactのフォーム処理は初めは複雑に感じるかもしれませんが、Controlled Componentsの概念を理解すれば、強力で柔軟なフォームを作成できます。実践を重ねることで、より洗練されたフォーム処理ができるようになります。