複雑なフォーム状態管理を、制御された(Controlled)コンポーネントと非制御(Uncontrolled)コンポーネントで管理する
Min-jun Kim
Dev Intern · Leapcell

フロントエンド開発の進化し続ける状況において、フォームはユーザーインタラクションの基盤であり続けています。シンプルなログイン画面から複雑な複数ステップのデータ入力フォームまで、スムーズで予測可能なユーザーエクスペリエンスのためには、効果的な状態管理が不可欠です。フォームが複雑になるにつれて、「制御された(controlled)」コンポーネントと「非制御(uncontrolled)」コンポーネントのどちらを選択するかは、この状態管理へのアプローチ方法をしばしば左右します。それぞれの従来のアプローチのニュアンスと実際的な意味を理解することは、アプリケーションの保守性、拡張性、パフォーマンに大きく影響します。この記事では、制御されたコンポーネントと非制御コンポーネントのコアコンセプトを掘り下げ、複雑なフォーム開発の文脈におけるそれらの応用とトレードオフについて説明します。
コアコンセプトと応用
複雑なフォームシナリオに飛び込む前に、基本的なコンセプトである「制御されたコンポーネント」と「非制御コンポーネント」を明確に理解しましょう。
制御された(Controlled)コンポーネント
制御されたコンポーネントとは、Reactの状態によって完全に管理されるフォーム要素(<input>
、<textarea>
、<select>
など)のことです。すべて状態の変更には、関連するハンドラー関数が必要です。Reactは、入力の状態の「単一の真実の情報源」となります。
原理:
コンポーネントの値は常にprops
によって駆動されます。ユーザーが入力したり操作したりすると、onChange
ハンドラーがコンポーネントの状態を更新し、それが入力のvalue
propを更新します。
実装例:
シンプルなテキスト入力を考えてみましょう:
import React, { useState } from 'react'; function ControlledTextInput() { const [value, setValue] = useState(''); const handleChange = (event) => { setValue(event.target.value); }; return ( <div> <label htmlFor="controlledInput">Controlled Input:</label> <input type="text" id="controlledInput" value={value} onChange={handleChange} /> <p>Current Value: {value}</p> </div> ); } export default ControlledTextInput;
複雑なフォームでの利点:
- 即時の検証とフィードバック: 検証ロジックは、キーストロークごとに実行でき、ユーザーに即時のフィードバックを提供します。
- 簡単なリセットと事前入力: 状態を更新するだけで、フォームを初期値に簡単にリセットしたり、データを事前入力したりできます。
- 集中管理された状態管理: すべてのフォームデータはコンポーネントの状態に存在するため、アプリケーションの他の部分とのアクセス、操作、同期が容易になります。
- 条件付きフィールドレンダリング: 他のフィールドの値に基づいてフィールドを表示または非表示にすることが、すべてのフォームデータに直接アクセスできるため、簡単になります。
非制御(Uncontrolled)コンポーネント
非制御コンポーネントとは、DOM自体によって管理されるフォーム要素のことです。通常はref
を使用して、必要なときに現在の値にアクセスします。Reactは入力の値を直接指定しません。
原理:
コンポーネントは、従来のHTMLフォーム要素により近い動作をします。その現在の値は、値が変更された可能性がある後にDOMをポーリングすることによってアクセスされます。
実装例:
useRef
フックを使用した入力:
import React, { useRef } from 'react'; function UncontrolledTextInput() { const inputRef = useRef(null); const handleSubmit = (event) => { event.preventDefault(); alert(`Current Value: ${inputRef.current.value}`); }; return ( <form onSubmit={handleSubmit}> <label htmlFor="uncontrolledInput">Uncontrolled Input:</label> <input type="text" id="uncontrolledInput" defaultValue="Initial Value" // Use defaultValue instead of value ref={inputRef} /> <button type="submit">Submit</button> </form> ); } export default UncontrolledTextInput;
複雑なフォームでの利点:
- シンプルなフォームではより簡単: 提出時に値のみが必要な非常に基本的なフォームでは、
onChange
ハンドラーと状態管理の必要性を減らすことで、非制御コンポーネントはコードを簡素化できます。 - パフォーマンスの可能性: 入力が非常に頻繁に変更され、リアルタイムの検証や同期が必要ないシナリオでは、すべてのキーストロークで状態更新を回避することが、わずかなパフォーマンス上の利点を提供する可能性があります(ただし、現代のReactではしばしば無視できる程度です)。
- サードパーティDOMライブラリとの統合: DOMを直接操作するライブラリ(例:特定のレガシー日付ピッカーやリッチテキストエディター)と統合する場合、非制御コンポーネントの方が扱いやすい場合があります。
複雑なフォームへの適用
複雑なフォームは、しばしば多数のフィールド、動的なセクション、複雑な検証ルール、フィールド間の相互依存関係を伴います。
複雑なフォームでの制御されたコンポーネント:
複数ステップの登録フォームの場合:
import React, { useState } from 'react'; function ComplexRegistrationForm() { const [formData, setFormData] = useState({ firstName: '', lastName: '', email: '', password: '', confirmPassword: '', newsletter: false, country: 'USA', }); const [errors, setErrors] = useState({}); const handleChange = (e) => { const { name, value, type, checked } = e.target; setFormData((prevData) => ({ ...prevData, [name]: type === 'checkbox' ? checked : value, })); // Real-time validation (simplified) if (errors[name]) { setErrors((prevErrors) => ({ ...prevErrors, [name]: '', // Clear error once user starts typing })); } }; const validateForm = () => { let newErrors = {}; if (!formData.firstName) newErrors.firstName = 'First name is required.'; if (!formData.email.includes('@')) newErrors.email = 'Invalid email address.'; if (formData.password.length < 6) newErrors.password = 'Password must be at least 6 characters.'; if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = 'Passwords do not match.'; setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = (e) => { e.preventDefault(); if (validateForm()) { console.log('Form Submitted:', formData); // Proceed with API call or further logic } else { console.log('Form has validation errors.'); } }; return ( <form onSubmit={handleSubmit} className="complex-form"> <h2>Registration Form</h2> <div className="form-group"> <label htmlFor="firstName">First Name:</label> <input type="text" id="firstName" name="firstName" value={formData.firstName} onChange={handleChange} /> {errors.firstName && <p className="error">{errors.firstName}</p>} </div> <div className="form-group"> <label htmlFor="email">Email:</label> <input type="email" id="email" name="email" value={formData.email} onChange={handleChange} /> {errors.email && <p className="error">{errors.email}</p>} </div> {/* ... other fields like password, confirm password, newsletter, country */} <button type="submit">Register</button> </form> ); } export default ComplexRegistrationForm;
この例では、すべてのフォームデータは単一のformData
状態オブジェクトに保持されています。これにより、以下が可能になります:
- 集中管理された検証:
validateForm
関数は、クロスフィールド検証を実行するためにすべてのフィールドに簡単にアクセスできます。 - 動的なUI:
confirmPassword
などのフィールドは、password
に基づいて条件付きでレンダリングまたは検証できます。 - フォームライブラリとの簡単な統合: FormikやReact Hook Form(ただし、後者は主にパフォーマンスのために非制御に焦点を当てていますが、制御されたラッパーを提供します)などのライブラリは、この制御されたアプローチをしばしば活用したり、それとうまく連携するAPIを提供したりします。
複雑なフォームでの非制御コンポーネント:
完全に複雑なフォームではあまり一般的ではありませんが、非制御コンポーネントは特定の要素、またはuseRef
と組み合わせて、ファイルアップロードのようなパフォーマンスが重要な入力、またはevery keystrokeでの親コンポーネントの再レンダリングがコスト高になる可能性のある非常に大きなリスト内の入力に役立つ場合があります。
import React, { useRef } from 'react'; function UncontrolledFileUpload() { const fileInputRef = useRef(null); const commentRef = useRef(null); // Simple uncontrolled text input const handleSubmit = (event) => { event.preventDefault(); const files = fileInputRef.current.files; const comment = commentRef.current.value; console.log('Uploaded Files:', files); console.log('Comment:', comment); // Logic to upload files and comments }; return ( <form onSubmit={handleSubmit}> <h2>Uncontrolled File Upload</h2> <div className="form-group"> <label htmlFor="fileUpload">Upload Files:</label> <input type="file" id="fileUpload" name="fileUpload" multiple ref={fileInputRef} /> </div> <div className="form-group"> <label htmlFor="comment">Your Comment:</label> <textarea id="comment" name="comment" defaultValue="Add your thoughts..." ref={commentRef} ></textarea> </div> <button type="submit">Upload</button> </form> ); } export default UncontrolledFileUpload;
この場合、file
入力はデフォルトで本質的に非制御です(Reactは値を簡単に管理しません)。また、textarea
については、提出時に単にその値を取得しています。このアプローチは、しばしばReact Hook Formのような特定のライブラリと組み合わされます。これはパフォーマンスのために非制御コンポーネントを内部的に促進し、開発者が手動でのuseRef
管理を抽象化しながら、効率的に入力を登録してその値を取得できるようにします。
ハイブリッドアプローチとフォームライブラリ:
実際には、多くの複雑なフォームはハイブリッドアプローチまたは専用のフォームライブラリの利用から恩恵を受けます。React Hook Formのようなライブラリは、パフォーマンスのために非制御コンポーネントを内部的に活用することが多く、開発者が堅牢な検証とエラー処理機能を提供しながら、効率的に入力を登録し、提出時にその値を取得できるようにします。これにより、開発者は両方の利点、すなわち非制御入力のパフォーマンス上の利点と、構造化されたフォームライブラリの開発者エクスペリエンス上の利点を享受できます。
結論
制御されたコンポーネントと非制御コンポーネントの選択は、特に複雑なフォーム開発においては、厳格な「どちらか一方」の決定ではありません。制御されたコンポーネントは、優れた制御、リアルタイム検証、予測可能な状態同期を提供し、複雑なロジックとリッチなユーザーエクスペリエンスを必要とするフォームに最適です。一方、非制御コンポーネントは、孤立した入力にシンプルさをもたらし、特定のシナリオでパフォーマンス上の利点を提供できます。最終的には、両方のパラダイムを理解し、それらをいつ適用するか(しばしば堅牢なフォーム管理ライブラリによって補強される)は、高品質で保守可能でユーザーフレンドリーな複雑なフォームを構築するための基盤となります。