React inputの実装|フォーム入力の基本から応用まで
Reactでのinput要素の実装方法を基本から応用まで詳しく解説。制御コンポーネント、バリデーション、カスタムインプットの作成方法を実践的に紹介
みなさん、Reactでフォームを作る時に困ったことはありませんか?
「inputの値が思うように更新されない」「バリデーションの実装がうまくいかない」そんな悩みを感じたことがある方も多いはずです。
実は、Reactでのフォーム実装には少しコツがあるんです。 この記事では、Reactでのinput要素の実装方法を基本から応用まで、分かりやすくお伝えします。 制御コンポーネントの作り方からカスタムインプットまで、実践的な内容をご紹介しますので、ぜひ参考にしてくださいね。
Reactでinputを扱う前に知っておきたいこと
Reactでinput要素を扱う際の基本的な考え方を理解しましょう。
従来のHTMLとは少し違うアプローチが必要なんです。
制御コンポーネントと非制御コンポーネントの違い
Reactでは、input要素を扱う方法が2つあります。
まずは両方のパターンを見てみましょう。
// ❌ 非制御コンポーネント(推奨されない)
function UncontrolledInput() {
const inputRef = useRef();
const handleSubmit = () => {
console.log(inputRef.current.value); // DOMから直接値を取得
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleSubmit}>送信</button>
</div>
);
}
このコードでは、useRef
を使ってDOMから直接値を取得しています。
でも、この方法はReactらしくないんです。
// ✅ 制御コンポーネント(推奨)
function ControlledInput() {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value); // Reactの状態で値を管理
};
const handleSubmit = () => {
console.log(value); // 状態から値を取得
};
return (
<div>
<input
type="text"
value={value}
onChange={handleChange}
/>
<button onClick={handleSubmit}>送信</button>
</div>
);
}
こちらの方法では、ReactのuseState
で値を管理しています。
制御コンポーネントと呼ばれるこの方法が、Reactでは推奨されています。
なぜなら、Reactの状態管理と一体化することで、より予測可能で管理しやすいコードになるからです。
基本的なinput要素の実装
それでは、様々なタイプのinput要素を実装してみましょう。
まずは全体像をご覧ください。
function BasicInputForm() {
const [formData, setFormData] = useState({
text: '',
email: '',
password: '',
number: 0,
date: '',
checkbox: false,
radio: '',
textarea: '',
select: ''
});
// 汎用的な変更ハンドラ
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form Data:', formData);
};
return (
<form onSubmit={handleSubmit}>
{/* 各種input要素 */}
</form>
);
}
ちょっと長いですが、大丈夫です! 一つずつ見ていきますね。
まず、状態管理の部分から。
const [formData, setFormData] = useState({
text: '',
email: '',
password: '',
// 他のフィールド...
});
フォームの全ての値をオブジェクトで管理しています。 この方法だと、フィールドが増えても管理しやすいんです。
次に、変更ハンドラを見てみましょう。
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
この関数が、すべてのinput要素で使えるんです。
type
によってチェックボックスかどうかを判断しています。
テキスト入力の実装はこんな感じです。
<div>
<label htmlFor="text">テキスト:</label>
<input
id="text"
name="text"
type="text"
value={formData.text}
onChange={handleChange}
placeholder="テキストを入力"
/>
</div>
value
で現在の値を設定し、onChange
で変更を検知します。
name
属性が重要で、これが状態のキーと対応しています。
チェックボックスの場合は少し違います。
<div>
<label>
<input
name="checkbox"
type="checkbox"
checked={formData.checkbox}
onChange={handleChange}
/>
利用規約に同意する
</label>
</div>
value
ではなくchecked
を使うのがポイントです。
input状態管理のベストプラクティス
効率的なinput状態管理にはいくつかのパターンがあります。
どの方法を選ぶかで、コードの見通しが大きく変わるんです。
パターン1: 個別の状態管理
function IndividualStateForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState('');
return (
<form>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="名前"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="メール"
/>
<input
value={age}
onChange={(e) => setAge(e.target.value)}
placeholder="年齢"
/>
</form>
);
}
フィールドが少ない場合はシンプルで分かりやすいです。 でも、フィールドが増えると管理が大変になります。
パターン2: オブジェクトでの一括管理
function ObjectStateForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
age: ''
});
const handleChange = (field) => (e) => {
setFormData(prev => ({
...prev,
[field]: e.target.value
}));
};
return (
<form>
<input
value={formData.name}
onChange={handleChange('name')}
placeholder="名前"
/>
<input
value={formData.email}
onChange={handleChange('email')}
placeholder="メール"
/>
<input
value={formData.age}
onChange={handleChange('age')}
placeholder="年齢"
/>
</form>
);
}
中規模のフォームに適しています。 関数をカリー化することで、各フィールド専用のハンドラを作っています。
パターン3: useReducerを使った管理
function formReducer(state, action) {
switch (action.type) {
case 'UPDATE_FIELD':
return {
...state,
[action.field]: action.value
};
case 'RESET_FORM':
return action.initialState;
default:
return state;
}
}
まず、reducerを定義します。
UPDATE_FIELD
でフィールドを更新し、RESET_FORM
でリセットできます。
function ReducerStateForm() {
const initialState = { name: '', email: '', age: '' };
const [formData, dispatch] = useReducer(formReducer, initialState);
const handleChange = (field) => (e) => {
dispatch({
type: 'UPDATE_FIELD',
field,
value: e.target.value
});
};
const handleReset = () => {
dispatch({
type: 'RESET_FORM',
initialState
});
};
return (
<form>
<input
value={formData.name}
onChange={handleChange('name')}
placeholder="名前"
/>
<input
value={formData.email}
onChange={handleChange('email')}
placeholder="メール"
/>
<input
value={formData.age}
onChange={handleChange('age')}
placeholder="年齢"
/>
<button type="button" onClick={handleReset}>
リセット
</button>
</form>
);
}
複雑な状態変更が必要な場合は、useReducer
が便利です。
アクションを定義することで、状態変更のパターンが明確になります。
バリデーション機能の実装
フォームには必ずバリデーションが必要ですよね。
ユーザーの入力をチェックして、適切にフィードバックする機能を作ってみましょう。
リアルタイムバリデーションの基本
まずは、入力と同時にチェックするバリデーションです。
全体の構造はこんな感じになります。
function ValidatedForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
// バリデーション関数
const validateField = (name, value) => {
// 各フィールドのチェック処理
};
// その他のハンドラ
}
フォームデータに加えて、errors
とtouched
も管理しています。
touched
は「ユーザーがそのフィールドに触れたか」を記録するためです。
バリデーション関数を詳しく見てみましょう。
const validateField = (name, value) => {
switch (name) {
case 'name':
if (!value.trim()) return '名前は必須です';
if (value.length < 2) return '名前は2文字以上で入力してください';
return '';
case 'email':
if (!value.trim()) return 'メールアドレスは必須です';
const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
if (!emailRegex.test(value)) return '正しいメールアドレスを入力してください';
return '';
case 'password':
if (!value) return 'パスワードは必須です';
if (value.length < 8) return 'パスワードは8文字以上で入力してください';
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
return 'パスワードは大文字、小文字、数字を含む必要があります';
}
return '';
case 'confirmPassword':
if (!value) return 'パスワード確認は必須です';
if (value !== formData.password) return 'パスワードが一致しません';
return '';
default:
return '';
}
};
各フィールドごとに異なるチェックを行っています。 エラーがなければ空文字を返すのがポイントです。
入力の変更ハンドラはこうなります。
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// リアルタイムバリデーション
if (touched[name]) {
const error = validateField(name, value);
setErrors(prev => ({
...prev,
[name]: error
}));
}
};
touched[name]
がtrue
の場合のみバリデーションを実行します。
これで、ユーザーが一度フィールドを触った後からチェックが始まります。
blur時(フィールドからフォーカスが外れた時)の処理も重要です。
const handleBlur = (e) => {
const { name, value } = e.target;
setTouched(prev => ({
...prev,
[name]: true
}));
const error = validateField(name, value);
setErrors(prev => ({
...prev,
[name]: error
}));
};
フィールドを離れた時にtouched
をtrue
にして、バリデーションを実行します。
カスタムバリデーションフックの作成
繰り返し使えるバリデーション機能を作ってみましょう。
まずは、フックの全体像をご覧ください。
function useFormValidation(initialValues, validationRules) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
// バリデーション関数
const validateField = useCallback((name, value) => {
const rules = validationRules[name];
if (!rules) return '';
// 各種チェック処理
}, [validationRules, values]);
// その他の関数
}
このフックを使えば、どんなフォームでも簡単にバリデーションが追加できます。
validateField
関数の中身を見てみましょう。
const validateField = useCallback((name, value) => {
const rules = validationRules[name];
if (!rules) return '';
// required チェック
if (rules.required && (!value || value.toString().trim() === '')) {
return rules.required;
}
// 値が空の場合、required以外のバリデーションはスキップ
if (!value || value.toString().trim() === '') {
return '';
}
// minLength チェック
if (rules.minLength && value.length < rules.minLength.value) {
return rules.minLength.message;
}
// maxLength チェック
if (rules.maxLength && value.length > rules.maxLength.value) {
return rules.maxLength.message;
}
// pattern チェック
if (rules.pattern && !rules.pattern.value.test(value)) {
return rules.pattern.message;
}
// custom バリデーション
if (rules.custom) {
return rules.custom(value, values);
}
return '';
}, [validationRules, values]);
ルールオブジェクトに基づいて、段階的にチェックしています。
custom
ルールでは、独自のバリデーション関数を実行できます。
使用例はこんな感じです。
function RegistrationForm() {
const validationRules = {
username: {
required: 'ユーザー名は必須です',
minLength: {
value: 3,
message: 'ユーザー名は3文字以上で入力してください'
},
pattern: {
value: /^[a-zA-Z0-9_]+$/,
message: 'ユーザー名は英数字とアンダースコアのみ使用可能です'
}
},
email: {
required: 'メールアドレスは必須です',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: '正しいメールアドレスを入力してください'
}
},
password: {
required: 'パスワードは必須です',
minLength: {
value: 8,
message: 'パスワードは8文字以上で入力してください'
},
pattern: {
value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
message: 'パスワードは大文字、小文字、数字を含む必要があります'
}
},
confirmPassword: {
required: 'パスワード確認は必須です',
custom: (value, values) => {
if (value !== values.password) {
return 'パスワードが一致しません';
}
return '';
}
}
};
const {
values,
setValue,
setTouched,
validateAllFields,
reset,
getFieldError,
isValid
} = useFormValidation(
{ username: '', email: '', password: '', confirmPassword: '' },
validationRules
);
// フォームの実装
}
ルールを定義するだけで、複雑なバリデーションが簡単に実装できます。
非同期バリデーション
サーバーでのチェックが必要な場合もありますよね。
ユーザー名の重複チェックなどを実装してみましょう。
function AsyncValidationForm() {
const [formData, setFormData] = useState({
username: '',
email: ''
});
const [errors, setErrors] = useState({});
const [validating, setValidating] = useState({});
// ユーザー名の重複チェック
const validateUsername = useCallback(
debounce(async (username) => {
if (!username || username.length < 3) return;
setValidating(prev => ({ ...prev, username: true }));
try {
const response = await fetch(`/api/check-username/${username}`);
const data = await response.json();
setErrors(prev => ({
...prev,
username: data.available ? '' : 'このユーザー名は既に使用されています'
}));
} catch (error) {
setErrors(prev => ({
...prev,
username: 'ユーザー名の確認に失敗しました'
}));
} finally {
setValidating(prev => ({ ...prev, username: false }));
}
}, 500),
[]
);
// 入力変更時の処理
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// リアルタイム非同期バリデーション
if (name === 'username') {
validateUsername(value);
}
};
return (
<form>
<div className="form-group">
<label htmlFor="username">ユーザー名</label>
<div className="input-container">
<input
id="username"
name="username"
type="text"
value={formData.username}
onChange={handleChange}
placeholder="ユーザー名を入力"
/>
{validating.username && (
<span className="validating-indicator">確認中...</span>
)}
</div>
{errors.username && (
<span className="error-message">{errors.username}</span>
)}
</div>
</form>
);
}
debounce
関数で入力の間隔を制御しています。
これで、ユーザーが入力を止めてから0.5秒後にチェックが実行されます。
デバウンス関数はこんな感じです。
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
連続した入力に対して、最後の入力から指定時間経過後に関数を実行します。
カスタムインプットコンポーネントの作成
再利用可能なinputコンポーネントを作ってみましょう。
一度作っておけば、プロジェクト全体で使い回せて便利です。
汎用的なInputコンポーネント
まずは、基本的なInputコンポーネントの全体像をご覧ください。
const Input = forwardRef(({
label,
error,
type = 'text',
required = false,
placeholder,
helperText,
startIcon,
endIcon,
className = '',
...props
}, ref) => {
const [isFocused, setIsFocused] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const inputId = useId();
const isPassword = type === 'password';
// イベントハンドラやその他の処理
return (
<div className={`input-wrapper ${className}`}>
{/* 実際のJSX */}
</div>
);
});
機能豊富なコンポーネントですが、使う側はシンプルに扱えます。
パスワード表示切り替えの処理を見てみましょう。
const handleFocus = (e) => {
setIsFocused(true);
props.onFocus?.(e);
};
const handleBlur = (e) => {
setIsFocused(false);
props.onBlur?.(e);
};
const togglePasswordVisibility = () => {
setShowPassword(!showPassword);
};
const inputType = isPassword && showPassword ? 'text' : type;
フォーカスの状態を管理して、パスワードの表示/非表示を切り替えています。
実際のJSX部分はこうなります。
return (
<div className={`input-wrapper ${className}`}>
{label && (
<label htmlFor={inputId} className="input-label">
{label}
{required && <span className="required-marker">*</span>}
</label>
)}
<div className={`input-container ${isFocused ? 'focused' : ''} ${error ? 'error' : ''}`}>
{startIcon && (
<span className="input-icon start-icon">{startIcon}</span>
)}
<input
ref={ref}
id={inputId}
type={inputType}
placeholder={placeholder}
className="input-field"
onFocus={handleFocus}
onBlur={handleBlur}
{...props}
/>
{isPassword && (
<button
type="button"
className="input-icon end-icon password-toggle"
onClick={togglePasswordVisibility}
tabIndex={-1}
>
{showPassword ? '🙈' : '👁️'}
</button>
)}
{endIcon && !isPassword && (
<span className="input-icon end-icon">{endIcon}</span>
)}
</div>
{error && (
<span className="error-message">{error}</span>
)}
{helperText && !error && (
<span className="helper-text">{helperText}</span>
)}
</div>
);
ラベル、アイコン、エラーメッセージ、ヘルプテキストなど、必要な要素を条件的に表示しています。
使用例はこんな感じです。
function CustomInputDemo() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
search: ''
});
const handleChange = (field) => (e) => {
setFormData(prev => ({
...prev,
[field]: e.target.value
}));
};
return (
<div className="form-demo">
<h2>カスタムインプットデモ</h2>
<Input
label="ユーザー名"
value={formData.username}
onChange={handleChange('username')}
placeholder="ユーザー名を入力"
required
helperText="3文字以上で入力してください"
startIcon="👤"
/>
<Input
label="パスワード"
type="password"
value={formData.password}
onChange={handleChange('password')}
placeholder="パスワードを入力"
required
helperText="8文字以上で入力してください"
/>
</div>
);
}
シンプルな記述で、機能豊富なinputが使えます。
数値専用のInputコンポーネント
数値入力専用のコンポーネントも作ってみましょう。
数値の入力にはいくつかの特殊な要件があります。
const NumberInput = forwardRef(({
label,
value,
onChange,
min,
max,
step = 1,
precision = 0,
prefix,
suffix,
allowNegative = true,
placeholder,
error,
...props
}, ref) => {
const [displayValue, setDisplayValue] = useState('');
const [isFocused, setIsFocused] = useState(false);
// 数値のフォーマット
const formatNumber = (num) => {
if (num === null || num === undefined || num === '') return '';
const number = parseFloat(num);
if (isNaN(number)) return '';
return precision > 0 ? number.toFixed(precision) : number.toString();
};
// 表示値の更新
useEffect(() => {
setDisplayValue(formatNumber(value));
}, [value, precision]);
// その他の処理
});
表示用の値と実際の値を分けて管理しているのがポイントです。
入力の変更処理を見てみましょう。
const handleInputChange = (e) => {
let inputValue = e.target.value;
// 負の値が許可されていない場合は除去
if (!allowNegative) {
inputValue = inputValue.replace('-', '');
}
// 数値以外の文字を除去(小数点、負号は保持)
inputValue = inputValue.replace(/[^0-9.-]/g, '');
// 複数の小数点を防ぐ
const decimalCount = (inputValue.match(/\./g) || []).length;
if (decimalCount > 1) {
inputValue = inputValue.substring(0, inputValue.lastIndexOf('.'));
}
setDisplayValue(inputValue);
// 数値に変換して onChange を呼び出し
const numericValue = parseFloat(inputValue);
if (!isNaN(numericValue)) {
// min/max の制限をチェック
let clampedValue = numericValue;
if (min !== undefined) clampedValue = Math.max(min, clampedValue);
if (max !== undefined) clampedValue = Math.min(max, clampedValue);
onChange?.(clampedValue);
} else if (inputValue === '' || inputValue === '-') {
onChange?.(null);
}
};
入力値を正規化して、範囲チェックも行っています。
増減ボタンの実装はこうなります。
const increment = () => {
const newValue = (value || 0) + step;
const clampedValue = max !== undefined ? Math.min(max, newValue) : newValue;
onChange?.(clampedValue);
};
const decrement = () => {
const newValue = (value || 0) - step;
const clampedValue = min !== undefined ? Math.max(min, newValue) : newValue;
onChange?.(clampedValue);
};
数値の増減も範囲内に収まるようにしています。
自動サイズ調整テキストエリア
テキストエリアも便利な機能を追加してみましょう。
内容に応じて高さが自動調整されるテキストエリアです。
const AutoResizeTextarea = forwardRef(({
label,
value = '',
onChange,
placeholder,
error,
minRows = 3,
maxRows = 10,
showCharCount = false,
maxLength,
helperText,
required = false,
...props
}, ref) => {
const textareaRef = useRef();
const [charCount, setCharCount] = useState(0);
// refの統合
const combinedRef = useMemo(() => {
return (node) => {
textareaRef.current = node;
if (typeof ref === 'function') {
ref(node);
} else if (ref) {
ref.current = node;
}
};
}, [ref]);
// 高さの自動調整
const adjustHeight = useCallback(() => {
const textarea = textareaRef.current;
if (!textarea) return;
// 一時的にheightをautoにして自然な高さを取得
textarea.style.height = 'auto';
// 行の高さを計算
const lineHeight = parseInt(getComputedStyle(textarea).lineHeight);
const minHeight = lineHeight * minRows;
const maxHeight = lineHeight * maxRows;
// スクロール高さを取得
const scrollHeight = textarea.scrollHeight;
// 制限内で高さを設定
const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);
textarea.style.height = `${newHeight}px`;
}, [minRows, maxRows]);
// 値の変更時に高さを調整
useEffect(() => {
adjustHeight();
setCharCount(value.length);
}, [value, adjustHeight]);
// その他の処理
});
自動サイズ調整の仕組みは、一度height: auto
にしてからscrollHeight
を取得し、最小・最大行数の範囲内で高さを設定しています。
Markdownエディタの機能も追加してみましょう。
function MarkdownEditor({ label, value, onChange, ...props }) {
const [isPreviewMode, setIsPreviewMode] = useState(false);
const [previewContent, setPreviewContent] = useState('');
// Markdownのプレビュー生成(簡易版)
const generatePreview = (markdown) => {
return markdown
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*)\*/gim, '<em>$1</em>')
.replace(/
/gim, '<br>');
};
useEffect(() => {
if (isPreviewMode) {
setPreviewContent(generatePreview(value || ''));
}
}, [value, isPreviewMode]);
const togglePreview = () => {
setIsPreviewMode(!isPreviewMode);
};
return (
<div className="markdown-editor">
<div className="markdown-editor-header">
<span>{label}</span>
<button
type="button"
onClick={togglePreview}
className="preview-toggle"
>
{isPreviewMode ? '編集' : 'プレビュー'}
</button>
</div>
{isPreviewMode ? (
<div
className="markdown-preview"
dangerouslySetInnerHTML={{ __html: previewContent }}
/>
) : (
<AutoResizeTextarea
value={value}
onChange={onChange}
placeholder="Markdownで入力してください..."
showCharCount
maxLength={5000}
minRows={5}
maxRows={20}
helperText="**太字** *斜体* # 見出し1 ## 見出し2"
{...props}
/>
)}
</div>
);
}
編集モードとプレビューモードを切り替えられるMarkdownエディタです。 実際のプロジェクトでは、より高機能なMarkdownパーサーを使うことをおすすめします。
フォームライブラリとの統合
Reactのフォームライブラリとカスタムコンポーネントを組み合わせてみましょう。
より効率的で保守性の高いフォームが作れます。
React Hook Formとの統合
まずは、React Hook Formと組み合わせる方法です。
Controllerコンポーネントを使って、カスタムコンポーネントを統合します。
import { useForm, Controller } from 'react-hook-form';
// React Hook Formで使用可能なInput wrapper
const FormInput = ({ name, control, rules, ...props }) => {
return (
<Controller
name={name}
control={control}
rules={rules}
render={({ field, fieldState: { error } }) => (
<Input
{...field}
{...props}
error={error?.message}
/>
)}
/>
);
};
Controllerで包むことで、カスタムコンポーネントでもReact Hook Formの機能が使えます。
数値入力コンポーネントの場合は少し注意が必要です。
const FormNumberInput = ({ name, control, rules, ...props }) => {
return (
<Controller
name={name}
control={control}
rules={rules}
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
<NumberInput
{...field}
{...props}
value={value}
onChange={onChange}
error={error?.message}
/>
)}
/>
);
};
onChange
とvalue
を明示的に受け取って、NumberInputに渡しています。
実際の使用例はこうなります。
function ReactHookFormExample() {
const {
control,
handleSubmit,
formState: { errors, isSubmitting },
reset,
watch
} = useForm({
defaultValues: {
name: '',
email: '',
age: null,
bio: '',
terms: false
}
});
const watchAge = watch('age');
const onSubmit = async (data) => {
console.log('Form Data:', data);
// 模擬的な非同期処理
await new Promise(resolve => setTimeout(resolve, 2000));
alert('フォームが送信されました!');
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h2>React Hook Form統合例</h2>
<FormInput
name="name"
control={control}
label="名前"
placeholder="お名前を入力"
rules={{
required: '名前は必須です',
minLength: {
value: 2,
message: '名前は2文字以上で入力してください'
}
}}
required
/>
<FormNumberInput
name="age"
control={control}
label="年齢"
min={0}
max={120}
suffix="歳"
rules={{
required: '年齢は必須です',
min: {
value: 18,
message: '18歳以上である必要があります'
}
}}
required
/>
{watchAge && watchAge < 18 && (
<div className="warning-message">
⚠️ 18歳未満の方は保護者の同意が必要です
</div>
)}
<div className="form-actions">
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '送信中...' : '送信'}
</button>
<button type="button" onClick={() => reset()}>
リセット
</button>
</div>
</form>
);
}
React Hook Formの機能(バリデーション、状態管理、送信処理など)がすべて使えます。
watch
で特定のフィールドの値を監視することもできて便利です。
Formikとの統合
Formikライブラリとの組み合わせも見てみましょう。
YupとFormikを組み合わせたバリデーションが特徴的です。
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
// Formik用のInput wrapper
const FormikInput = ({ label, ...props }) => (
<Field>
{({ field, meta }) => (
<Input
{...field}
{...props}
label={label}
error={meta.touched && meta.error ? meta.error : ''}
/>
)}
</Field>
);
// バリデーションスキーマ
const validationSchema = Yup.object({
name: Yup.string()
.min(2, '名前は2文字以上で入力してください')
.required('名前は必須です'),
email: Yup.string()
.email('正しいメールアドレスを入力してください')
.required('メールアドレスは必須です'),
age: Yup.number()
.positive('年齢は正の数で入力してください')
.integer('年齢は整数で入力してください')
.min(18, '18歳以上である必要があります')
.max(120, '120歳以下で入力してください')
.required('年齢は必須です'),
});
Yupのスキーマは宣言的で読みやすいのが特徴です。
実際のフォーム実装はこうなります。
function FormikExample() {
const handleSubmit = async (values, { setSubmitting, resetForm }) => {
console.log('Form Data:', values);
// 模擬的な非同期処理
await new Promise(resolve => setTimeout(resolve, 2000));
alert('フォームが送信されました!');
resetForm();
setSubmitting(false);
};
return (
<Formik
initialValues={{
name: '',
email: '',
age: '',
}}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ isSubmitting, values, setFieldValue }) => (
<Form>
<h2>Formik統合例</h2>
<FormikInput
name="name"
label="名前"
placeholder="お名前を入力"
required
/>
<FormikInput
name="email"
type="email"
label="メールアドレス"
placeholder="example@mail.com"
required
/>
<Field name="age">
{({ field, meta }) => (
<NumberInput
label="年齢"
value={field.value}
onChange={(value) => setFieldValue('age', value)}
onBlur={() => setFieldTouched('age', true)}
min={0}
max={120}
suffix="歳"
error={meta.touched && meta.error ? meta.error : ''}
required
/>
)}
</Field>
<div className="form-actions">
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '送信中...' : '送信'}
</button>
</div>
</Form>
)}
</Formik>
);
}
FormikもReact Hook Formも、それぞれに良さがあります。 プロジェクトの要件に応じて選択してくださいね。
まとめ
Reactでのinput実装について、基本から応用まで見てきました。
最後に、重要なポイントをまとめておきますね。
覚えておきたい重要なポイント
input実装で特に大切なことをまとめてみました。
制御コンポーネントの活用
Reactの状態でinputの値を管理することで、予測可能で管理しやすいコードになります。
useState
やuseReducer
を使って、フォームの状態を適切に管理しましょう。
適切なバリデーション
リアルタイムバリデーションと非同期バリデーションを組み合わせることで、ユーザーフレンドリーなフォームを作れます。
touched
状態を使って、適切なタイミングでエラーを表示するのがコツです。
再利用可能なコンポーネント カスタムinputコンポーネントを作ることで、開発効率が大幅に向上します。 アクセシビリティやユーザビリティも考慮して設計しましょう。
ライブラリとの組み合わせ React Hook FormやFormikなどのライブラリと組み合わせることで、より効率的な開発が可能になります。 プロジェクトの規模や要件に応じて選択してください。
プロジェクトに応じたパターン選択
どの実装パターンを選ぶべきか、目安をお伝えしますね。
// 小規模なフォーム(3-5フィールド)
// - useState での直接管理
// - 基本的なバリデーション
// - シンプルな実装で十分
// 中規模なフォーム(6-15フィールド)
// - カスタムフックでの状態管理
// - 汎用的なバリデーション
// - 再利用可能なコンポーネント
// 大規模なフォーム(16フィールド以上)
// - React Hook Form / Formik の活用
// - 複雑なバリデーションルール
// - カスタムコンポーネントライブラリ
// エンタープライズレベル
// - デザインシステムとの統合
// - 国際化対応
// - アクセシビリティ完全対応
規模に応じて適切な手法を選ぶことが大切です。
これからの学習ステップ
input実装をさらに極めるための学習の進め方をご提案します。
1. 基本の定着
制御コンポーネントの理解を深めて、useState
やuseReducer
での状態管理をマスターしましょう。
基本的なバリデーションの実装パターンも覚えておくと役立ちます。
2. 実践的な機能の習得 カスタムフックの作成方法を学んで、非同期バリデーションの実装にもチャレンジしてみてください。 複雑なinputコンポーネントの作成も経験値になります。
3. ライブラリの活用 React Hook FormやFormikの使い方を覚えて、バリデーションライブラリとの組み合わせも試してみましょう。 UIコンポーネントライブラリとの統合も実践的です。
4. 高度なテクニック パフォーマンス最適化やアクセシビリティ対応、テスト手法まで学べば、プロレベルの実装ができるようになります。
Reactでのinput実装は、フロントエンド開発の基礎となる重要なスキルです。
制御コンポーネントの概念をしっかりと理解して、実践的なバリデーションやカスタムコンポーネントの作成方法を身につけることで、ユーザーにとって使いやすいフォームが作れるようになります。
今回紹介した実装例を参考にして、ぜひあなたのプロジェクトで活用してみてください。 継続的に学習することで、どんどん上達していけるはずです。