404 Not Found

404 Not Found


nginx

フォームと制御コンポーネント

フォームは、Webアプリケーションとユーザーをつなぐ架け橋の役割を果たします。Reactでは、フォームはもはや単に「入力して送信する」だけのものではありません。その代わりに、「Reactはユーザーが入力する文字をすべて把握しています」。このパターンは制御コンポーネントと呼ばれます。


1. 学習内容



2. 登録フォームの変遷

(1) 課題:管理対象外のフォームからのデータ追跡が困難

アリスは、ネイティブメソッドを使用してデータを取得するユーザー登録フォームを作成しました:

HTML
<!-- HTML Form:Read data all at once upon submission -->
<form id="register-form">
  <input name="username" id="username" />
  <input type="email" name="email" id="email" />
  <input type="password" name="password" id="password" />
  <select name="role" id="role">
    <option value="developer">Developer</option>
    <option value="designer">Designer</option>
  </select>
  <button type="submit">Register</button>
</form>

<script>
  document.getElementById('register-form').addEventListener('submit', function(e) {
    e.preventDefault()
    // I'll go when I submit it. DOM Data from Zhongna
    const username = document.getElementById('username').value
    const email = document.getElementById('email').value
    const password = document.getElementById('password').value
    const role = document.getElementById('role').value
    // ... Manually verify each field
  })
</script>

質問:

(2) Reactの制御コンポーネント向けのソリューション

JSX
function RegisterForm() {
  const [form, setForm] = React.useState({
    username: '',
    email: '',
    password: '',
    role: 'developer'
  })

  // Each input change React Everyone knows
  function handleChange(e) {
    const { name, value } = e.target
    setForm(prev => ({ ...prev, [name]: value }))
  }

  function handleSubmit(e) {
    e.preventDefault()
    console.log('Registration Data:', form)  // Retrieve the complete form data
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" value={form.username} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
      <input name="password" value={form.password} onChange={handleChange} />
      <select name="role" value={form.role} onChange={handleChange}>
        <option value="developer">Developer</option>
        <option value="designer">Designer</option>
      </select>
      <button type="submit">Register</button>
    </form>
  )
}

メリット:Reactは各入力フィールドをリアルタイムで追跡し、リアルタイムのバリデーションとリアルタイムのデータ同期を実現するとともに、送信と同時に完全なデータを即座に取得します。



3. 制御対象コンポーネントと非制御対象コンポーネント

100%
graph TB
    subgraph "Controlled Components(Recommendations)"
        A1[User Input] --> B1[onChange Event]
        B1 --> C1[Update State]
        C1 --> D1[React Render Again]
        D1 --> E1[value = State]
    end
    
    subgraph "Uncontrolled Components"
        A2[User Input] --> B2[Update Directly DOM]
        B2 --> C2[Use when submitting ref Reading]
    end
    
    style C1 fill:#52c41a,color:#fff
    style B2 fill:#faad14,color:#000
次元 制御対象のコンポーネント 制御対象外のコンポーネント
データソース Reactのステート(唯一の真実の源) DOM自体
アクセス方法 ステートから直接読み込む ref または document.getElementById が必要
リアルタイム検証 ✅ ネイティブ対応 ❌ 追加の監視が必要
即時同期 ✅ リアルタイムのデータ同期 ❌ 手動での同期が必要
ユースケース 一般的なフォーム ファイルのアップロード、簡単な単発のフォーム

ルール:90%のケースでは、制御済みコンポーネントを使用すること。ファイル <input type="file"> のみ、制御されていないコンポーネントを使用しなければならない。



4. さまざまなフォーム要素に対する制御構文

フォーム要素 関連するプロパティ onChange の値
<input type="text"> value e.target.value <input value={name} onChange={e => setName(e.target.value)} />
<input type="number"> value Number(e.target.value) 手動で数値に変換する必要があります
<input type="checkbox"> checked e.target.checked ブール値、値なし
<input type="radio"> checked e.target.value 名前でグループ化されています。「checked」は、それらが選択されているかどうかを制御します
<textarea> value e.target.value Reactは子要素のテキストではなく値を使用する
<select> value e.target.value 複数選択 multiple + 配列
<input type="file"> ❌ 制御なし e.target.files[0] 制御なしでのみ使用可能

(1) テキスト入力

JSX
function TextInputs() {
  const [name, setName] = React.useState('')
  const [bio, setBio] = React.useState('')
  const [age, setAge] = React.useState(18)

  return (
    <div>
      {/* Single-line text */}
      <label>Name:</label>
      <input
        type="text"
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="Please enter your name"
      />

      {/* Numbers */}
      <label>Age:</label>
      <input
        type="number"
        value={age}
        onChange={e => setAge(Number(e.target.value))}  // Convert to a number!
        min={0} max={150}
      />

      {/* Multi-line text */}
      <label>Personal Profile:</label>
      <textarea
        value={bio}
        onChange={e => setBio(e.target.value)}
        rows={4}
        placeholder="Introduce Yourself..."
      />
    </div>
  )
}

:Reactでは、子要素のテキストコンテンツの代わりに<textarea>プロパティを使用してください。<input type="number">e.target.valueの値は文字列ですので、必ず数値に変換してください。

(2) チェックボックス

JSX
function SelectInputs() {
  const [city, setCity] = React.useState('')
  const [hobbies, setHobbies] = React.useState([])

  return (
    <div>
      {/* Single-selection drop-down menu */}
      <label>City:</label>
      <select value={city} onChange={e => setCity(e.target.value)}>
        <option value="">-- Please select --</option>
        <option value="beijing">Beijing</option>
        <option value="shanghai">Shanghai</option>
        <option value="shenzhen">Shenzhen</option>
      </select>

      {/* Multiple-choice drop-down list */}
      <label>Hobbies:</label>
      <select
        multiple
        value={hobbies}
        onChange={e => {
          const selected = Array.from(e.target.options)
            .filter(o => o.selected)
            .map(o => o.value)
          setHobbies(selected)
        }}
      >
        <option value="coding">Programming</option>
        <option value="reading">Read</option>
        <option value="gaming">Games</option>
      </select>
    </div>
  )
}

(3) チェックボックスとラジオボタン

JSX
function CheckboxAndRadio() {
  const [agree, setAgree] = React.useState(false)
  const [gender, setGender] = React.useState('')
  const [interests, setInterests] = React.useState([])

  // Multiple Choice:Toggle Inclusion/Exclusion
  function toggleInterest(value) {
    setInterests(prev =>
      prev.includes(value)
        ? prev.filter(i => i !== value)
        : [...prev, value]
    )
  }

  return (
    <div>
      {/* Single checkbox */}
      <label>
        <input
          type="checkbox"
          checked={agree}
          onChange={e => setAgree(e.target.checked)}  // Please note that checked No value
        />
        Agree to the User Agreement
      </label>
      {agree && <p style={{ color: '#52c41a' }}>✅ Agreed</p>}

      {/* Radio Button Group:The same one name */}
      <p>Gender:</p>
      {['Male', 'Female', 'Other'].map(g => (
        <label key={g} style={{ marginRight: '12px' }}>
          <input
            type="radio"
            name="gender"
            value={g}
            checked={gender === g}
            onChange={e => setGender(e.target.value)}
          />
          {g}
        </label>
      ))}

      {/* Multiple-Selection Checkbox Group */}
      <p>Areas of Interest:</p>
      {['Front End', 'Backend', 'AI', 'Design'].map(item => (
        <label key={item} style={{ marginRight: '12px' }}>
          <input
            type="checkbox"
            checked={interests.includes(item)}
            onChange={() => toggleInterest(item)}
          />
          {item}
        </label>
      ))}
    </div>
  )
}

(1) ▶ サンプル:フォーム要素タイプのクイックリファレンス表

JSX
// ============================================
// Example:Controlled Syntax for All Common Form Elements
// ============================================

function FormElementReference() {
  const [text, setText] = React.useState('')
  const [number, setNumber] = React.useState(0)
  const [email, setEmail] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [bio, setBio] = React.useState('')
  const [city, setCity] = React.useState('')
  const [agree, setAgree] = React.useState(false)
  const [gender, setGender] = React.useState('')
  const [file, setFile] = React.useState(null)

  return (
    <div>
      {/* Text Input */}
      <input type="text" value={text} onChange={e => setText(e.target.value)} />
      
      {/* Numeric Input */}
      <input type="number" value={number} onChange={e => setNumber(Number(e.target.value))} />
      
      {/* Enter your email address */}
      <input type="email" value={email} onChange={e => setEmail(e.target.value)} />
      
      {/* Password Entry */}
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} />
      
      {/* Multi-line text */}
      <textarea value={bio} onChange={e => setBio(e.target.value)} />
      
      {/* Drop-down menu */}
      <select value={city} onChange={e => setCity(e.target.value)}>
        <option value="">Please select</option>
        <option value="beijing">Beijing</option>
      </select>
      
      {/* Single checkbox */}
      <input type="checkbox" checked={agree} onChange={e => setAgree(e.target.checked)} />
      
      {/* Radio button */}
      <input type="radio" name="gender" value="male" checked={gender === 'male'}
        onChange={e => setGender(e.target.value)} />
      
      {/* File Upload(Uncontrolled) */}
      <input type="file" onChange={e => setFile(e.target.files[0])} />
      {/* Note:File input must be in uncontrolled mode. */}
    </div>
  )
}
▶ 試してみよう

5. フォームの検証

検証のタイミング トリガーとなる事象 メリット デメリット 推奨されるシナリオ
入力中 onChange リアルタイムのフィードバック。ユーザーは誤りを即座に修正できる 頻繁にトリガーされる。ユーザーが入力を完了する前にエラーが通知される パスワードの強度、ユーザー名の検証
フォーカスが外れたとき onBlur 注意散漫を防ぐ;完了後にのみ検証を行う ユーザーはフィールドからカーソルを外してから初めてエラーを確認する ほとんどのフォームフィールド
送信時 onSubmit 最終確認と統合検証 ユーザーがフォームに入力した後になって初めてエラーに気づく 最終的なフォールバック検証

(1) リアルタイム検証

JSX
function ValidatedForm() {
  const [email, setEmail] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [errors, setErrors] = React.useState({})
  const [touched, setTouched] = React.useState({})  // Record which fields have been accessed

  function validate(fieldName, value) {
    const newErrors = { ...errors }
    
    if (fieldName === 'email') {
      if (!value) newErrors.email = 'The email address cannot be left blank.'
      else if (!value.includes('@')) newErrors.email = 'The email address format is incorrect.'
      else delete newErrors.email
    }
    
    if (fieldName === 'password') {
      if (!value) newErrors.password = 'The password cannot be empty.'
      else if (value.length < 6) newErrors.password = 'Password must be at least 6 chars'
      else if (!/[A-Z]/.test(value)) newErrors.password = 'Passwords must contain uppercase letters'
      else delete newErrors.password
    }

    setErrors(newErrors)
  }

  function handleBlur(e) {
    const { name, value } = e.target
    setTouched({ ...touched, [name]: true })
    validate(name, value)
  }

  function handleSubmit(e) {
    e.preventDefault()
    // Validate all fields before submitting
    const allErrors = {}
    if (!email || !email.includes('@')) allErrors.email = 'Please enter a valid email address'
    if (!password || password.length < 6) allErrors.password = 'Password must be at least 6 chars'
    
    if (Object.keys(allErrors).length > 0) {
      setErrors(allErrors)
      setTouched({ email: true, password: true })
      return
    }

    alert('Submission Successful!')
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: '400px' }}>
      <div style={{ marginBottom: '16px' }}>
        <label>Email:</label>
        <input
          name="email"
          type="email"
          value={email}
          onChange={e => { setEmail(e.target.value); validate('email', e.target.value) }}
          onBlur={handleBlur}
          style={inputStyle(touched.email && errors.email)}
        />
        {touched.email && errors.email && (
          <p style={{ color: '#ff4d4f', fontSize: '12px', margin: '4px 0' }}>{errors.email}</p>
        )}
      </div>

      <div style={{ marginBottom: '16px' }}>
        <label>Password:</label>
        <input
          name="password"
          type="password"
          value={password}
          onChange={e => { setPassword(e.target.value); validate('password', e.target.value) }}
          onBlur={handleBlur}
          style={inputStyle(touched.password && errors.password)}
        />
        {touched.password && errors.password && (
          <p style={{ color: '#ff4d4f', fontSize: '12px', margin: '4px 0' }}>{errors.password}</p>
        )}
      </div>

      {/* Password Strength Indicator */}
      {password && (
        <div style={{ marginBottom: '16px' }}>
          <p style={{ fontSize: '12px', color: '#666' }}>
            Password Strength:
            <span style={{ color: password.length < 6 ? '#ff4d4f' : password.length < 10 ? '#faad14' : '#52c41a' }}>
              {password.length < 6 ? 'Weak' : password.length < 10 ? 'Medium' : 'Strong'}
            </span>
          </p>
          <div style={{ height: '4px', backgroundColor: '#f0f0f0', borderRadius: '2px' }}>
            <div style={{
              height: '100%',
              width: `${Math.min(100, (password.length / 12) * 100)}%`,
              backgroundColor: password.length < 6 ? '#ff4d4f' : password.length < 10 ? '#faad14' : '#52c41a',
              borderRadius: '2px',
              transition: 'width 0.3s'
            }} />
          </div>
        </div>
      )}

      <button type="submit" disabled={Object.keys(errors).length > 0}
        style={{
          width: '100%', padding: '10px',
          backgroundColor: Object.keys(errors).length > 0 ? '#d9d9d9' : '#1890ff',
          color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
        }}>
        Register
      </button>
    </form>
  )
}

function inputStyle(hasError) {
  return {
    width: '100%', padding: '8px',
    border: `1px solid ${hasError ? '#ff4d4f' : '#d9d9d9'}`,
    borderRadius: '4px',
    outline: 'none'
  }
}

(2) useForm フックをカスタマイズする

フォームのロジックをカスタムフックに抽出することで、再利用を容易にします:

JSX
// ============================================
// Example:Custom useForm Hook
// Features:Unified Management of Form Status、Verification、Submit
// ============================================

function useForm(initialValues, validateFn) {
  const [values, setValues] = React.useState(initialValues)
  const [errors, setErrors] = React.useState({})
  const [touched, setTouched] = React.useState({})

  // Handling Input Changes
  function handleChange(e) {
    const { name, value, type, checked } = e.target
    const newValue = type === 'checkbox' ? checked : value
    const newValues = { ...values, [name]: newValue }
    setValues(newValues)
    
    // Real-Time Verification
    if (validateFn) {
      const newErrors = validateFn(newValues)
      setErrors(newErrors)
    }
  }

  // Handling Loss of Focus
  function handleBlur(e) {
    const { name } = e.target
    setTouched({ ...touched, [name]: true })
  }

  // Process Submission
  function handleSubmit(callback) {
    return (e) => {
      e.preventDefault()
      // Mark all fields as checked
      const allTouched = Object.keys(values).reduce((acc, key) => {
        acc[key] = true
        return acc
      }, {})
      setTouched(allTouched)
      
      // Verification
      if (validateFn) {
        const newErrors = validateFn(values)
        setErrors(newErrors)
        if (Object.keys(newErrors).length > 0) return
      }
      
      callback(values)
    }
  }

  // Reset Form
  function reset() {
    setValues(initialValues)
    setErrors({})
    setTouched({})
  }

  return { values, errors, touched, handleChange, handleBlur, handleSubmit, reset }
}

// ---- Usage useForm ----
function LoginForm() {
  function validate(values) {
    const errors = {}
    if (!values.email) errors.email = 'Please enter your email address'
    else if (!values.email.includes('@')) errors.email = 'The email address format is incorrect.'
    if (!values.password) errors.password = 'Please enter your password'
    else if (values.password.length < 6) errors.password = 'Password must be at least 6 chars'
    return errors
  }

  const form = useForm({ email: '', password: '' }, validate)

  return (
    <form onSubmit={form.handleSubmit(data => console.log('Submit:', data))}
      style={{ maxWidth: '400px', margin: '0 auto' }}>
      <h2>Log In</h2>
      
      <input
        name="email"
        value={form.values.email}
        onChange={form.handleChange}
        onBlur={form.handleBlur}
        placeholder="Email"
        style={{ width: '100%', padding: '8px', marginBottom: '4px', border: `1px solid ${form.touched.email && form.errors.email ? '#ff4d4f' : '#d9d9d9'}`, borderRadius: '4px' }}
      />
      {form.touched.email && form.errors.email && (
        <p style={{ color: '#ff4d4f', fontSize: '12px', margin: '0 0 12px 0' }}>{form.errors.email}</p>
      )}

      <input
        name="password"
        type="password"
        value={form.values.password}
        onChange={form.handleChange}
        onBlur={form.handleBlur}
        placeholder="Password"
        style={{ width: '100%', padding: '8px', marginBottom: '4px', border: `1px solid ${form.touched.password && form.errors.password ? '#ff4d4f' : '#d9d9d9'}`, borderRadius: '4px' }}
      />
      {form.touched.password && form.errors.password && (
        <p style={{ color: '#ff4d4f', fontSize: '12px', margin: '0 0 12px 0' }}>{form.errors.password}</p>
      )}

      <button type="submit" style={{
        width: '100%', padding: '10px', backgroundColor: '#1890ff',
        color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
      }}>
        Log In
      </button>
      <button type="button" onClick={form.reset}
        style={{ width: '100%', padding: '8px', marginTop: '8px', cursor: 'pointer' }}>
        Reset
      </button>
    </form>
  )
}


6. 完全な例:ユーザー登録フォーム

JSX
// ============================================
// Complete Example:User Registration Form
// Features:Controlled Components + Real-Time Verification + Form Data Synchronization
// ============================================

function RegistrationForm() {
  const [form, setForm] = React.useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: '',
    country: '',
    gender: '',
    acceptTerms: false
  })
  const [errors, setErrors] = React.useState({})

  function validate() {
    const newErrors = {}
    if (!form.username || form.username.length < 2)
      newErrors.username = 'Username must be at least 2 characters'
    if (!form.email || !form.email.includes('@'))
      newErrors.email = 'Please enter a valid email address'
    if (!form.password || form.password.length < 6)
      newErrors.password = 'Password must be at least 6 chars'
    if (form.password !== form.confirmPassword)
      newErrors.confirmPassword = 'The two passwords do not match'
    if (!form.country)
      newErrors.country = 'Please select a country'
    if (!form.acceptTerms)
      newErrors.acceptTerms = 'Please agree to the Terms of Service'
    return newErrors
  }

  function handleChange(e) {
    const { name, value, type, checked } = e.target
    const newValue = type === 'checkbox' ? checked : value
    const newForm = { ...form, [name]: newValue }
    setForm(newForm)
    setErrors(validate())
    // Interconnection:Automatically adjust password rule prompts after selecting a country
  }

  function handleSubmit(e) {
    e.preventDefault()
    const finalErrors = validate()
    setErrors(finalErrors)
    if (Object.keys(finalErrors).length === 0) {
      alert(`Registration Successful!Welcome ${form.username}`)
    }
  }

  function inputStyle(fieldName) {
    return {
      width: '100%', padding: '8px', marginBottom: '4px',
      border: `1px solid ${errors[fieldName] ? '#ff4d4f' : '#d9d9d9'}`,
      borderRadius: '4px'
    }
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: '500px', margin: '0 auto' }}>
      <h2>📝 User Registration</h2>

      <input name="username" value={form.username} onChange={handleChange}
        placeholder="Username" style={inputStyle('username')} />
      {errors.username && <p style={errorStyle}>{errors.username}</p>}

      <input name="email" type="email" value={form.email} onChange={handleChange}
        placeholder="Email" style={inputStyle('email')} />
      {errors.email && <p style={errorStyle}>{errors.email}</p>}

      <input name="password" type="password" value={form.password} onChange={handleChange}
        placeholder="Password (at least 6 chars)" style={inputStyle('password')} />
      {errors.password && <p style={errorStyle}>{errors.password}</p>}

      <input name="confirmPassword" type="password" value={form.confirmPassword}
        onChange={handleChange} placeholder="Confirm Password" style={inputStyle('confirmPassword')} />
      {errors.confirmPassword && <p style={errorStyle}>{errors.confirmPassword}</p>}

      <select name="country" value={form.country} onChange={handleChange}
        style={inputStyle('country')}>
        <option value="">-- Select a Country --</option>
        <option value="CN">China</option>
        <option value="US">United States</option>
        <option value="JP">Japan</option>
        <option value="BR">Brazil</option>
        <option value="SA">Saudi Arabia</option>
      </select>
      {errors.country && <p style={errorStyle}>{errors.country}</p>}

      <div style={{ margin: '12px 0' }}>
        <p>Gender:</p>
        {['Male', 'Female', 'Other'].map(g => (
          <label key={g} style={{ marginRight: '16px' }}>
            <input type="radio" name="gender" value={g}
              checked={form.gender === g} onChange={handleChange} />
            {g}
          </label>
        ))}
      </div>

      <label style={{ display: 'block', margin: '12px 0' }}>
        <input type="checkbox" name="acceptTerms" checked={form.acceptTerms}
          onChange={handleChange} />
        I have read and agree to <a href="#">User Agreement</a>
      </label>
      {errors.acceptTerms && <p style={errorStyle}>{errors.acceptTerms}</p>}

      <button type="submit" style={{
        width: '100%', padding: '10px',
        backgroundColor: Object.keys(errors).length === 0 && form.username ? '#1890ff' : '#d9d9d9',
        color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer'
      }}>
        Register
      </button>
    </form>
  )
}

const errorStyle = {
  color: '#ff4d4f', fontSize: '12px',
  margin: '0 0 8px 0'
}

(1) ▶ サンプル:フォームデータのバインディング—国を選択した際に市外局番を自動的に入力する

JSX
function PhoneForm() {
  const [form, setForm] = useState({ country: 'US', phone: '' })
  const areaCodes = { US: '+1', CN: '+86', JP: '+81', UK: '+44', BR: '+55' }

  function handleCountryChange(e) {
    const country = e.target.value
    setForm(prev => ({
      ...prev,
      country,
      phone: prev.phone.replace(/^\+\d+\s*/, '') + ' ',
    }))
  }

  function handleChange(e) {
    const { name, value } = e.target
    setForm(prev => ({ ...prev, [name]: value }))
  }

  return (
    <div style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Phone Contact</h3>
      <select name="country" value={form.country} onChange={handleCountryChange}
        style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }}>
        {Object.keys(areaCodes).map(c => (
          <option key={c} value={c}>{c}</option>
        ))}
      </select>
      <div style={{ display: 'flex', gap: 8 }}>
        <input value={areaCodes[form.country]} readOnly style={{ width: 60, padding: 8, borderRadius: 4 }} />
        <input name="phone" value={form.phone} onChange={handleChange}
          placeholder="Phone number" style={{ flex: 1, padding: 8, borderRadius: 4 }} />
      </div>
      <p style={{ color: '#666', fontSize: 13, marginTop: 8 }}>
        Full: {areaCodes[form.country]} {form.phone.trim()}
      </p>
    </div>
  )
}
▶ 試してみよう

(2) ▶ サンプル:動的フォーム—チームメンバーの追加・削除

JSX
function TeamForm() {
  const [members, setMembers] = useState([{ name: '', email: '', role: 'developer' }])

  function addMember() {
    setMembers(prev => [...prev, { name: '', email: '', role: 'developer' }])
  }

  function removeMember(index) {
    setMembers(prev => prev.filter((_, i) => i !== index))
  }

  function updateMember(index, field, value) {
    setMembers(prev => prev.map((m, i) => i === index ? { ...m, [field]: value } : m))
  }

  function handleSubmit(e) {
    e.preventDefault()
    console.log('Team:', members)
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 500, margin: '0 auto' }}>
      <h3>Team Members</h3>
      {members.map((m, i) => (
        <div key={i} style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}>
          <input value={m.name} onChange={e => updateMember(i, 'name', e.target.value)}
            placeholder="Name" style={{ flex: 1, padding: 6, borderRadius: 4 }} />
          <input value={m.email} onChange={e => updateMember(i, 'email', e.target.value)}
            placeholder="Email" style={{ flex: 1, padding: 6, borderRadius: 4 }} />
          <select value={m.role} onChange={e => updateMember(i, 'role', e.target.value)}
            style={{ padding: 6, borderRadius: 4 }}>
            <option value="developer">Dev</option>
            <option value="designer">Design</option>
            <option value="manager">PM</option>
          </select>
          {members.length > 1 && (
            <button type="button" onClick={() => removeMember(i)}
              style={{ padding: '6px 10px', color: '#ff4d4f', border: 'none', cursor: 'pointer' }}>
              X
            </button>
          )}
        </div>
      ))}
      <button type="button" onClick={addMember}
        style={{ padding: '6px 16px', marginRight: 8, cursor: 'pointer' }}>
        + Add Member
      </button>
      <button type="submit" style={{ padding: '6px 16px', cursor: 'pointer' }}>Submit</button>
    </form>
  )
}
▶ 試してみよう

(3) ▶ サンプル:多段階フォームウィザード

JSX
function MultiStepForm() {
  const [step, setStep] = useState(1)
  const [form, setForm] = useState({ name: '', email: '', password: '', city: '', agree: false })
  const [errors, setErrors] = useState({})

  function updateField(e) {
    const { name, value, type, checked } = e.target
    setForm(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value }))
  }

  function validate() {
    const errs = {}
    if (step === 1) {
      if (!form.name.trim()) errs.name = 'Name required'
      if (!form.email.includes('@')) errs.email = 'Valid email required'
    } else if (step === 2) {
      if (form.password.length < 6) errs.password = 'At least 6 characters'
    } else if (step === 3) {
      if (!form.city) errs.city = 'Select a city'
      if (!form.agree) errs.agree = 'Must agree to terms'
    }
    setErrors(errs)
    return Object.keys(errs).length === 0
  }

  function next() { if (validate()) setStep(s => Math.min(s + 1, 3)) }
  function back() { setStep(s => Math.max(s - 1, 1)) }

  return (
    <div style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Step {step} of 3</h3>
      <div style={{ display: 'flex', gap: 4, marginBottom: 16 }}>
        {[1, 2, 3].map(s => (
          <div key={s} style={{ flex: 1, height: 4, borderRadius: 2,
            backgroundColor: s <= step ? '#1890ff' : '#f0f0f0' }} />
        ))}
      </div>
      {step === 1 && (
        <>
          <input name="name" value={form.name} onChange={updateField} placeholder="Name"
            style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4, border: errors.name ? '1px solid #ff4d4f' : undefined }} />
          {errors.name && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.name}</p>}
          <input name="email" value={form.email} onChange={updateField} placeholder="Email"
            style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4, border: errors.email ? '1px solid #ff4d4f' : undefined }} />
          {errors.email && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.email}</p>}
        </>
      )}
      {step === 2 && (
        <>
          <input name="password" type="password" value={form.password} onChange={updateField}
            placeholder="Password" style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4 }} />
          {errors.password && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.password}</p>}
        </>
      )}
      {step === 3 && (
        <>
          <select name="city" value={form.city} onChange={updateField}
            style={{ width: '100%', padding: 8, marginBottom: 4, borderRadius: 4 }}>
            <option value="">Select city</option>
            <option value="beijing">Beijing</option>
            <option value="shanghai">Shanghai</option>
          </select>
          {errors.city && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.city}</p>}
          <label style={{ display: 'block', marginTop: 8 }}>
            <input type="checkbox" name="agree" checked={form.agree} onChange={updateField} />
            I agree to the terms
          </label>
          {errors.agree && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{errors.agree}</p>}
        </>
      )}
      <div style={{ marginTop: 16 }}>
        {step > 1 && <button type="button" onClick={back} style={{ marginRight: 8, cursor: 'pointer' }}>Back</button>}
        {step < 3 ? (
          <button type="button" onClick={next} style={{ cursor: 'pointer' }}>Next</button>
        ) : (
          <button type="button" onClick={() => { if (validate()) alert('Done!') }} style={{ cursor: 'pointer' }}>Submit</button>
        )}
      </div>
    </div>
  )
}
▶ 試してみよう

(4) ▶ サンプル:非管理コンポーネント — ファイルのアップロードと useRef

JSX
function FileUploadForm() {
  const fileInputRef = useRef(null)
  const [preview, setPreview] = useState(null)
  const [fileName, setFileName] = useState('')

  function handleSubmit(e) {
    e.preventDefault()
    const file = fileInputRef.current?.files[0]
    if (!file) { alert('Please select a file'); return }
    console.log('Uploading:', file.name, file.size, 'bytes')
    alert(`File "${file.name}" (${(file.size / 1024).toFixed(1)} KB) selected for upload`)
  }

  function handleFileChange(e) {
    const file = e.target.files[0]
    if (!file) { setPreview(null); setFileName(''); return }
    setFileName(file.name)
    if (file.type.startsWith('image/')) {
      const reader = new FileReader()
      reader.onload = (e) => setPreview(e.target.result)
      reader.readAsDataURL(file)
    } else {
      setPreview(null)
    }
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>File Upload (Uncontrolled)</h3>
      <div style={{ marginBottom: 12 }}>
        <input type="file" ref={fileInputRef} onChange={handleFileChange}
          accept="image/*,.pdf,.doc" style={{ marginBottom: 8 }} />
        {fileName && <p style={{ fontSize: 13, color: '#666' }}>Selected: {fileName}</p>}
      </div>
      {preview && (
        <div style={{ marginBottom: 12 }}>
          <img src={preview} alt="Preview" style={{ maxWidth: '100%', maxHeight: 200, borderRadius: 4, border: '1px solid #eee' }} />
        </div>
      )}
      <button type="submit" style={{ padding: '8px 24px', cursor: 'pointer' }}>Upload</button>
    </form>
  )
}
▶ 試してみよう

❓ よくある質問

Q input[type="checkbox"] については、checkedvalue のどちらを使用すべきですか?
A チェックボックスの場合は、checked attribute (boolean), not value. To retrieve the new value in the corresponding onChange event, use e.target.checked. Similarly, for radio buttons, use checkedを使用してください。
Q フォームの検証はいつ行うべきですか?
A 3つのタイミングオプションはそれぞれ異なる目的を持っています:① ユーザーが入力中に検証(onChange) — リアルタイムのフィードバックを提供しますが、頻繁にトリガーされます; ② フィールドのフォーカスが外れたとき(onBlur)の検証 — ユーザーがフォームへの入力を終えてからエラーを表示することで、作業の妨げを最小限に抑えます; ③ 送信時(onSubmit)の検証 — 最終チェックとして機能します。onBlurとonSubmitを組み合わせて使用し、ユーザー名などの重要なフィールドにはリアルタイムのフィードバックを得るためにonChangeを追加することをお勧めします。
Q フォームに多くのフィールドがある場合、handleChange を扱う際の一般的なアプローチはどのようなものですか?
A e.target.name を、計算プロパティ名である const { name, value } = e.target; setForm(prev => ({...prev, [name]: value})) と組み合わせて使用します。こうすることで、すべての <input name="xxx"> で同じ handleChange を使用できるようになります。チェックボックスについては特別な処理が必要です:const newValue = type === 'checkbox' ? checked : value
Q 制御対象コンポーネントと非制御対象コンポーネントの区別はどのように決めるのですか?
A 簡単なルールがあります。リアルタイムの検証、書式設定、または条件付き表示が必要な場合 → 制御対象コンポーネントを使用します(例:リアルタイムのパスワード強度表示、金額の自動書式設定)。最終的な値のみが重要である場合や、送信時にのみ値を読み取る場合は → 非制御コンポーネントを使用します(例:ファイルのアップロード、単純な問い合わせフォーム)。<input type="file"> 非制御コンポーネントはvalueが読み取り専用であるため、これしか使用できません。useForm フックを使用することで、制御コンポーネントの定型コードをカプセル化できます。

📖 まとめ


📝 練習問題

  1. 基本演習(難易度 ⭐):名前、メールアドレス、評価(ドロップダウンオプション:肯定的/中立/否定的)、および内容(テキストエリア)を含む FeedbackForm コンポーネントを作成してください。送信されると、すべてのデータをアラートに表示してください。
  2. 上級問題(難易度 ⭐⭐):パスワードの強度(弱い/中程度/強い)をリアルタイムで表示する PasswordStrengthMeter コンポーネントを作成してください。ルール:8文字以上+大文字・小文字+数字=「強い」;6文字以上=「中程度」;それ以外=「弱い」。
  3. 課題(難易度:⭐⭐⭐):ユーザーがフォーム項目を動的に追加・削除できる DynamicForm コンポーネントを作成してください(例:「家族を複数人追加する」)。各フォーム項目には、名前、年齢、関係性の3つのフィールドを含める必要があります。送信時には、コンポーネントが配列を出力するようにしてください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%