pin_drop当前位置:知识文库 ❯ 图文

框架集成:06 React表单与HTML - 详细教程与实战指南


教程简介

表单是Web应用中最常见的交互方式,React对HTML表单的处理有着独特的设计理念。在React中,表单元素分为受控组件非受控组件两大类。受控组件将表单值与React状态绑定,通过状态驱动UI更新;非受控组件则通过ref直接访问DOM获取表单值。

本教程将详细介绍受控组件与非受控组件、ref获取表单值、表单验证、多表单处理以及常用表单库(Formik、React Hook Form)的集成方式。


核心概念

受控组件

受控组件是React推荐的表单处理方式,表单值由React状态完全控制。以下是一个完整的受控组件注册表单示例:

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>React受控组件</title>
  <style>
    body { font-family: -apple-system, sans-serif; padding: 20px; max-width: 900px; margin: 0 auto; background: #f5f5f5; }
    .panel { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin: 15px 0; }
    pre { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 13px; }
    .form-group { margin-bottom: 15px; }
    .form-group label { display: block; margin-bottom: 5px; font-weight: bold; color: #333; }
    .form-group input, .form-group select, .form-group textarea {
      width: 100%; padding: 10px 12px; border: 2px solid #e0e0e0; border-radius: 6px;
      font-size: 14px; transition: border-color 0.2s; box-sizing: border-box;
    }
    .form-group input:focus, .form-group select:focus, .form-group textarea:focus {
      outline: none; border-color: #667eea;
    }
    .form-group .error { color: #e74c3c; font-size: 13px; margin-top: 4px; }
    .form-group .hint { color: #999; font-size: 12px; margin-top: 4px; }
    .checkbox-group { display: flex; align-items: center; gap: 8px; }
    .checkbox-group input { width: auto; }
    .radio-group { display: flex; gap: 15px; }
    .radio-group label { display: flex; align-items: center; gap: 5px; font-weight: normal; cursor: pointer; }
    button { padding: 10px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 15px; }
    .btn-primary { background: #667eea; color: white; }
    .btn-primary:hover { background: #5a6fd6; }
    .btn-primary:disabled { background: #ccc; cursor: not-allowed; }
    .preview { background: #f8f9fa; padding: 15px; border-radius: 8px; border: 1px solid #e0e0e0; margin-top: 15px; }
    .preview h4 { margin-top: 0; color: #667eea; }
  </style>
</head>
<body>
  <h1>React受控组件</h1>
  <div id="controlled-demo"></div>

  <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <script type="text/babel">
    function ControlledForm() {
      const [formData, setFormData] = React.useState({
        username: '',
        email: '',
        password: '',
        gender: '',
        bio: '',
        subscribe: false,
        level: 'beginner'
      });

      const [errors, setErrors] = React.useState({});

      function handleChange(e) {
        const { name, value, type, checked } = e.target;
        setFormData(prev => ({
          ...prev,
          [name]: type === 'checkbox' ? checked : value
        }));
        if (errors[name]) {
          setErrors(prev => ({ ...prev, [name]: '' }));
        }
      }

      function validate() {
        const newErrors = {};
        if (!formData.username.trim()) newErrors.username = '用户名不能为空';
        else if (formData.username.length < 3) newErrors.username = '用户名至少3个字符';
        if (!formData.email.trim()) newErrors.email = '邮箱不能为空';
        else if (!/\S+@\S+\.\S+/.test(formData.email)) newErrors.email = '邮箱格式不正确';
        if (!formData.password) newErrors.password = '密码不能为空';
        else if (formData.password.length < 6) newErrors.password = '密码至少6个字符';
        if (!formData.gender) newErrors.gender = '请选择性别';
        setErrors(newErrors);
        return Object.keys(newErrors).length === 0;
      }

      function handleSubmit(e) {
        e.preventDefault();
        if (validate()) {
          alert('表单提交成功!\n' + JSON.stringify(formData, null, 2));
        }
      }

      return (
        <div>
          <div className="panel">
            <h3>受控组件 - 注册表单</h3>
            <p style={{ color: '#666', fontSize: '14px' }}>每个表单值都由React状态控制,通过onChange实时更新</p>
            <form onSubmit={handleSubmit}>
              <div className="form-group">
                <label>用户名</label>
                <input type="text" name="username" value={formData.username} onChange={handleChange} placeholder="请输入用户名" />
                {errors.username && <div className="error">{errors.username}</div>}
              </div>

              <div className="form-group">
                <label>邮箱</label>
                <input type="email" name="email" value={formData.email} onChange={handleChange} placeholder="请输入邮箱" />
                {errors.email && <div className="error">{errors.email}</div>}
              </div>

              <div className="form-group">
                <label>密码</label>
                <input type="password" name="password" value={formData.password} onChange={handleChange} placeholder="请输入密码" />
                {errors.password && <div className="error">{errors.password}</div>}
                <div className="hint">密码长度至少6位</div>
              </div>

              <div className="form-group">
                <label>性别</label>
                <div className="radio-group">
                  <label><input type="radio" name="gender" value="male" checked={formData.gender === 'male'} onChange={handleChange} /> 男</label>
                  <label><input type="radio" name="gender" value="female" checked={formData.gender === 'female'} onChange={handleChange} /> 女</label>
                </div>
                {errors.gender && <div className="error">{errors.gender}</div>}
              </div>

              <div className="form-group">
                <label>等级</label>
                <select name="level" value={formData.level} onChange={handleChange}>
                  <option value="beginner">初学者</option>
                  <option value="intermediate">中级</option>
                  <option value="advanced">高级</option>
                </select>
              </div>

              <div className="form-group">
                <label>个人简介</label>
                <textarea name="bio" value={formData.bio} onChange={handleChange} rows="3" placeholder="介绍一下你自己" />
              </div>

              <div className="form-group">
                <div className="checkbox-group">
                  <input type="checkbox" name="subscribe" checked={formData.subscribe} onChange={handleChange} id="subscribe" />
                  <label htmlFor="subscribe" style={{ fontWeight: 'normal' }}>订阅新闻通讯</label>
                </div>
              </div>

              <button type="submit" className="btn-primary">提交注册</button>
            </form>
          </div>

          <div className="panel">
            <div className="preview">
              <h4>实时状态预览</h4>
              <pre style={{ background: '#2d2d2d', color: '#f8f8f2', padding: '10px', borderRadius: '6px', fontSize: '12px' }}>
                {JSON.stringify(formData, null, 2)}
              </pre>
            </div>
          </div>
        </div>
      );
    }

    const root = ReactDOM.createRoot(document.getElementById('controlled-demo'));
    root.render(<ControlledForm />);
  </script>
</body>
</html>

非受控组件与ref

非受控组件通过ref直接访问DOM,表单值由DOM自身管理。适用于文件输入等需要直接操作DOM的场景:

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>非受控组件与ref</title>
  <style>
    body { font-family: -apple-system, sans-serif; padding: 20px; max-width: 900px; margin: 0 auto; background: #f5f5f5; }
    .panel { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin: 15px 0; }
    pre { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 13px; }
    .form-group { margin-bottom: 15px; }
    .form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
    .form-group input { width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 6px; font-size: 14px; box-sizing: border-box; }
    .form-group input:focus { outline: none; border-color: #667eea; }
    button { padding: 10px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 15px; margin: 5px; }
    .btn-primary { background: #667eea; color: white; }
    .btn-secondary { background: #e0e0e0; color: #333; }
    .comparison { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
    .comp-card { padding: 15px; border: 1px solid #e0e0e0; border-radius: 8px; }
    .comp-card h4 { margin-top: 0; }
    .controlled { border-color: #27ae60; background: #f0faf5; }
    .uncontrolled { border-color: #f39c12; background: #fffaf0; }
    .file-preview { margin-top: 10px; }
    .file-preview img { max-width: 200px; max-height: 150px; border-radius: 6px; }
  </style>
</head>
<body>
  <h1>非受控组件与ref</h1>
  <div id="uncontrolled-demo"></div>

  <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <script type="text/babel">
    function UncontrolledForm() {
      const nameRef = React.useRef(null);
      const emailRef = React.useRef(null);
      const fileRef = React.useRef(null);
      const [filePreview, setFilePreview] = React.useState(null);
      const [submittedData, setSubmittedData] = React.useState(null);

      function handleSubmit(e) {
        e.preventDefault();
        setSubmittedData({
          name: nameRef.current.value,
          email: emailRef.current.value,
          file: fileRef.current.files[0]?.name || '未选择文件'
        });
      }

      function handleFileChange() {
        const file = fileRef.current.files[0];
        if (file && file.type.startsWith('image/')) {
          const reader = new FileReader();
          reader.onload = (e) => setFilePreview(e.target.result);
          reader.readAsDataURL(file);
        } else {
          setFilePreview(null);
        }
      }

      function handleReset() {
        nameRef.current.value = '';
        emailRef.current.value = '';
        fileRef.current.value = '';
        setFilePreview(null);
        setSubmittedData(null);
      }

      return (
        <div>
          <div className="panel">
            <h3>非受控组件 - 使用ref获取表单值</h3>
            <form onSubmit={handleSubmit}>
              <div className="form-group">
                <label>姓名</label>
                <input type="text" ref={nameRef} defaultValue="默认姓名" placeholder="请输入姓名" />
              </div>
              <div className="form-group">
                <label>邮箱</label>
                <input type="email" ref={emailRef} placeholder="请输入邮箱" />
              </div>
              <div className="form-group">
                <label>头像上传(文件输入必须用非受控)</label>
                <input type="file" ref={fileRef} accept="image/*" onChange={handleFileChange} />
                {filePreview && (
                  <div className="file-preview">
                    <img src={filePreview} alt="预览" />
                  </div>
                )}
              </div>
              <button type="submit" className="btn-primary">提交</button>
              <button type="button" className="btn-secondary" onClick={handleReset}>重置</button>
            </form>
          </div>

          {submittedData && (
            <div className="panel">
              <h3>提交的数据</h3>
              <pre>{JSON.stringify(submittedData, null, 2)}</pre>
            </div>
          )}

          <div className="panel">
            <h3>受控 vs 非受控对比</h3>
            <div className="comparison">
              <div className="comp-card controlled">
                <h4 style={{ color: '#27ae60' }}>受控组件</h4>
                <ul>
                  <li>值由React状态管理</li>
                  <li>每次输入都触发onChange</li>
                  <li>可以实时验证</li>
                  <li>可以条件性禁用输入</li>
                  <li>强制设置输入值</li>
                </ul>
                <pre>{`<input
  value={name}
  onChange={e =>
    setName(e.target.value)
  }
/>`}</pre>
              </div>
              <div className="comp-card uncontrolled">
                <h4 style={{ color: '#f39c12' }}>非受控组件</h4>
                <ul>
                  <li>值由DOM自身管理</li>
                  <li>使用ref获取值</li>
                  <li>使用defaultValue设置初始值</li>
                  <li>适合文件输入等场景</li>
                  <li>与非React代码集成方便</li>
                </ul>
                <pre>{`<input
  ref={inputRef}
  defaultValue="初始值"
/>

// 获取值
const val = inputRef.current.value;`}</pre>
              </div>
            </div>
          </div>
        </div>
      );
    }

    const root = ReactDOM.createRoot(document.getElementById('uncontrolled-demo'));
    root.render(<UncontrolledForm />);
  </script>
</body>
</html>

表单验证与多表单处理

以下示例展示了如何结合自定义Hook实现表单验证、多步骤表单以及动态联系人列表等功能:

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>表单验证与多表单处理</title>
  <style>
    body { font-family: -apple-system, sans-serif; padding: 20px; max-width: 900px; margin: 0 auto; background: #f5f5f5; }
    .panel { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin: 15px 0; }
    .form-group { margin-bottom: 15px; }
    .form-group label { display: block; margin-bottom: 5px; font-weight: bold; font-size: 14px; }
    .form-group input, .form-group select { width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 6px; font-size: 14px; box-sizing: border-box; transition: border-color 0.2s; }
    .form-group input:focus, .form-group select:focus { outline: none; border-color: #667eea; }
    .form-group input.valid { border-color: #27ae60; }
    .form-group input.invalid { border-color: #e74c3c; }
    .error-msg { color: #e74c3c; font-size: 12px; margin-top: 4px; }
    .success-msg { color: #27ae60; font-size: 12px; margin-top: 4px; }
    .password-strength { height: 4px; border-radius: 2px; margin-top: 8px; transition: all 0.3s; }
    .strength-weak { background: #e74c3c; width: 33%; }
    .strength-medium { background: #f39c12; width: 66%; }
    .strength-strong { background: #27ae60; width: 100%; }
    button { padding: 10px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 15px; margin: 5px; }
    .btn-primary { background: #667eea; color: white; }
    .btn-add { background: #27ae60; color: white; }
    .btn-remove { background: #e74c3c; color: white; font-size: 13px; padding: 6px 12px; }
    .step-indicator { display: flex; justify-content: center; margin-bottom: 20px; }
    .step { padding: 8px 20px; border-radius: 20px; margin: 0 5px; font-size: 14px; }
    .step-active { background: #667eea; color: white; }
    .step-done { background: #27ae60; color: white; }
    .step-pending { background: #e0e0e0; color: #999; }
    .contact-row { display: flex; gap: 10px; align-items: flex-start; margin-bottom: 10px; padding: 10px; background: #f8f9fa; border-radius: 6px; }
    .contact-row input { flex: 1; }
  </style>
</head>
<body>
  <h1>表单验证与多表单处理</h1>
  <div id="validation-demo"></div>

  <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <script type="text/babel">
    // 自定义Hook:表单验证
    function useFormValidation(initialValues, validateFn) {
      const [values, setValues] = React.useState(initialValues);
      const [errors, setErrors] = React.useState({});
      const [touched, setTouched] = React.useState({});

      function handleChange(e) {
        const { name, value } = e.target;
        setValues(prev => ({ ...prev, [name]: value }));
        if (touched[name]) {
          const fieldErrors = validateFn({ ...values, [name]: value });
          setErrors(prev => ({ ...prev, [name]: fieldErrors[name] || '' }));
        }
      }

      function handleBlur(e) {
        const { name } = e.target;
        setTouched(prev => ({ ...prev, [name]: true }));
        const fieldErrors = validateFn(values);
        setErrors(prev => ({ ...prev, [name]: fieldErrors[name] || '' }));
      }

      function validateAll() {
        const allErrors = validateFn(values);
        setErrors(allErrors);
        setTouched(Object.keys(initialValues).reduce((acc, key) => ({ ...acc, [key]: true }), {}));
        return Object.keys(allErrors).every(key => !allErrors[key]);
      }

      return { values, errors, touched, handleChange, handleBlur, validateAll, setValues };
    }

    // 密码强度检测
    function getPasswordStrength(password) {
      if (!password) return 0;
      let strength = 0;
      if (password.length >= 6) strength++;
      if (/[A-Z]/.test(password)) strength++;
      if (/[0-9]/.test(password)) strength++;
      if (/[^A-Za-z0-9]/.test(password)) strength++;
      return strength;
    }

    function MultiStepForm() {
      const [step, setStep] = React.useState(1);
      const [contacts, setContacts] = React.useState([{ name: '', phone: '', relation: '' }]);

      const validate = (vals) => {
        const errs = {};
        if (step === 1) {
          if (!vals.fullName?.trim()) errs.fullName = '请输入姓名';
          if (!vals.email?.trim()) errs.email = '请输入邮箱';
          else if (!/\S+@\S+\.\S+/.test(vals.email)) errs.email = '邮箱格式不正确';
          if (!vals.password) errs.password = '请输入密码';
          else if (vals.password.length < 6) errs.password = '密码至少6位';
        }
        if (step === 2) {
          if (!vals.city?.trim()) errs.city = '请输入城市';
          if (!vals.address?.trim()) errs.address = '请输入地址';
        }
        return errs;
      };

      const { values, errors, touched, handleChange, handleBlur, validateAll } = useFormValidation(
        { fullName: '', email: '', password: '', city: '', address: '' },
        validate
      );

      function nextStep() {
        if (validateAll()) setStep(s => s + 1);
      }

      function prevStep() {
        setStep(s => s - 1);
      }

      function addContact() {
        setContacts([...contacts, { name: '', phone: '', relation: '' }]);
      }

      function removeContact(index) {
        setContacts(contacts.filter((_, i) => i !== index));
      }

      function updateContact(index, field, value) {
        const updated = [...contacts];
        updated[index][field] = value;
        setContacts(updated);
      }

      function handleSubmit(e) {
        e.preventDefault();
        if (validateAll()) {
          alert('提交成功!\n' + JSON.stringify({ ...values, contacts }, null, 2));
        }
      }

      const passwordStrength = getPasswordStrength(values.password);
      const strengthLabel = ['', '弱', '中', '强', '很强'][passwordStrength];
      const strengthClass = ['', 'strength-weak', 'strength-medium', 'strength-strong', 'strength-strong'][passwordStrength];

      return (
        <div className="panel">
          <h3>多步骤表单</h3>
          <div className="step-indicator">
            <div className={`step ${step >= 1 ? (step > 1 ? 'step-done' : 'step-active') : 'step-pending'}`}>
              {step > 1 ? '✓' : '1'} 基本信息
            </div>
            <div className={`step ${step >= 2 ? (step > 2 ? 'step-done' : 'step-active') : 'step-pending'}`}>
              {step > 2 ? '✓' : '2'} 地址信息
            </div>
            <div className={`step ${step >= 3 ? 'step-active' : 'step-pending'}`}>
              3 紧急联系人
            </div>
          </div>

          <form onSubmit={handleSubmit}>
            {step === 1 && (
              <div>
                <div className="form-group">
                  <label>姓名</label>
                  <input name="fullName" value={values.fullName} onChange={handleChange} onBlur={handleBlur}
                    className={touched.fullName ? (errors.fullName ? 'invalid' : 'valid') : ''} placeholder="请输入姓名" />
                  {touched.fullName && errors.fullName && <div className="error-msg">{errors.fullName}</div>}
                </div>
                <div className="form-group">
                  <label>邮箱</label>
                  <input name="email" type="email" value={values.email} onChange={handleChange} onBlur={handleBlur}
                    className={touched.email ? (errors.email ? 'invalid' : 'valid') : ''} placeholder="请输入邮箱" />
                  {touched.email && errors.email && <div className="error-msg">{errors.email}</div>}
                  {touched.email && !errors.email && values.email && <div className="success-msg">邮箱格式正确</div>}
                </div>
                <div className="form-group">
                  <label>密码</label>
                  <input name="password" type="password" value={values.password} onChange={handleChange} onBlur={handleBlur}
                    className={touched.password ? (errors.password ? 'invalid' : 'valid') : ''} placeholder="请输入密码" />
                  {values.password && <div className={`password-strength ${strengthClass}`}></div>}
                  {values.password && <div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>密码强度: {strengthLabel}</div>}
                  {touched.password && errors.password && <div className="error-msg">{errors.password}</div>}
                </div>
                <button type="button" className="btn-primary" onClick={nextStep}>下一步</button>
              </div>
            )}

            {step === 2 && (
              <div>
                <div className="form-group">
                  <label>城市</label>
                  <input name="city" value={values.city} onChange={handleChange} onBlur={handleBlur}
                    className={touched.city ? (errors.city ? 'invalid' : 'valid') : ''} placeholder="请输入城市" />
                  {touched.city && errors.city && <div className="error-msg">{errors.city}</div>}
                </div>
                <div className="form-group">
                  <label>详细地址</label>
                  <input name="address" value={values.address} onChange={handleChange} onBlur={handleBlur}
                    className={touched.address ? (errors.address ? 'invalid' : 'valid') : ''} placeholder="请输入详细地址" />
                  {touched.address && errors.address && <div className="error-msg">{errors.address}</div>}
                </div>
                <button type="button" className="btn-primary" onClick={prevStep}>上一步</button>
                <button type="button" className="btn-primary" onClick={nextStep}>下一步</button>
              </div>
            )}

            {step === 3 && (
              <div>
                <h4>紧急联系人</h4>
                {contacts.map((contact, index) => (
                  <div key={index} className="contact-row">
                    <input placeholder="姓名" value={contact.name} onChange={e => updateContact(index, 'name', e.target.value)} />
                    <input placeholder="电话" value={contact.phone} onChange={e => updateContact(index, 'phone', e.target.value)} />
                    <select value={contact.relation} onChange={e => updateContact(index, 'relation', e.target.value)}>
                      <option value="">关系</option>
                      <option value="parent">父母</option>
                      <option value="spouse">配偶</option>
                      <option value="friend">朋友</option>
                    </select>
                    {contacts.length > 1 && (
                      <button type="button" className="btn-remove" onClick={() => removeContact(index)}>删除</button>
                    )}
                  </div>
                ))}
                <button type="button" className="btn-add" onClick={addContact}>添加联系人</button>
                <div style={{ marginTop: '15px' }}>
                  <button type="button" className="btn-primary" onClick={prevStep}>上一步</button>
                  <button type="submit" className="btn-primary">提交</button>
                </div>
              </div>
            )}
          </form>
        </div>
      );
    }

    const root = ReactDOM.createRoot(document.getElementById('validation-demo'));
    root.render(<MultiStepForm />);
  </script>
</body>
</html>

表单库简介

原生React表单处理需要大量样板代码:状态管理、验证逻辑、错误处理、提交处理等。表单库封装了这些通用逻辑,让开发者专注于业务。以下是React Hook Form和Formik两大主流表单库的使用示例:

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>React表单库</title>
  <style>
    body { font-family: -apple-system, sans-serif; padding: 20px; max-width: 900px; margin: 0 auto; background: #f5f5f5; }
    .panel { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin: 15px 0; }
    pre { background: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 13px; }
    .lib-card { border: 1px solid #e0e0e0; border-radius: 12px; padding: 20px; margin: 15px 0; }
    .lib-card h3 { margin-top: 0; }
    .lib-tag { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: bold; margin-right: 5px; }
    .tag-formik { background: #61dafb; color: #000; }
    .tag-rhf { background: #ec5990; color: white; }
    .tag-native { background: #27ae60; color: white; }
    table { width: 100%; border-collapse: collapse; margin: 10px 0; }
    th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
    th { background: #667eea; color: white; }
    .feature-list { columns: 2; }
    .feature-list li { margin: 5px 0; }
  </style>
</head>
<body>
  <h1>React表单库简介</h1>

  <div class="panel">
    <h2>为什么需要表单库?</h2>
    <p>原生React表单处理需要大量样板代码:状态管理、验证逻辑、错误处理、提交处理等。表单库封装了这些通用逻辑,让开发者专注于业务。</p>
  </div>

  <div class="lib-card">
    <h3><span class="lib-tag tag-rhf">推荐</span> React Hook Form</h3>
    <p>基于Hook的表单库,性能优秀,体积小。</p>
    <pre>{`import { useForm } from 'react-hook-form';

function LoginForm() {
  const {
    register,        // 注册表单字段
    handleSubmit,    // 处理表单提交
    formState: { errors, isSubmitting },  // 表单状态
    watch,           // 监听字段值变化
    reset,           // 重置表单
    setValue,        // 设置字段值
    getValues,       // 获取字段值
  } = useForm({
    defaultValues: {
      email: '',
      password: '',
    }
  });

  const onSubmit = (data) => {
    console.log(data); // { email: '...', password: '...' }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('email', {
          required: '邮箱不能为空',
          pattern: {
            value: /\\S+@\\S+\\.\\S+/,
            message: '邮箱格式不正确'
          }
        })}
      />
      {errors.email && <span>{errors.email.message}</span>}

      <input
        type="password"
        {...register('password', {
          required: '密码不能为空',
          minLength: { value: 6, message: '密码至少6位' }
        })}
      />
      {errors.password && <span>{errors.password.message}</span>}

      <button type="submit" disabled={isSubmitting}>登录</button>
    </form>
  );
}`}</pre>
    <h4>核心优势</h4>
    <ul class="feature-list">
      <li>非受控为主,减少不必要的重渲染</li>
      <li>体积小(~8KB gzipped)</li>
      <li>内置验证规则</li>
      <li>支持Yup/Zod等schema验证</li>
      <li>与UI库集成方便</li>
      <li>TypeScript友好</li>
    </ul>
  </div>

  <div class="lib-card">
    <h3><span class="lib-tag tag-formik">经典</span> Formik</h3>
    <p>最流行的React表单库之一,基于受控组件。</p>
    <pre>{`import { useFormik } from 'formik';

function LoginForm() {
  const formik = useFormik({
    initialValues: { email: '', password: '' },
    validate: (values) => {
      const errors = {};
      if (!values.email) errors.email = '必填';
      else if (!/\\S+@\\S+\\.\\S+/.test(values.email))
        errors.email = '邮箱格式不正确';
      return errors;
    },
    onSubmit: (values) => {
      console.log(values);
    },
  });

  return (
    <form onSubmit={formik.handleSubmit}>
      <input
        name="email"
        value={formik.values.email}
        onChange={formik.handleChange}
        onBlur={formik.handleBlur}
      />
      {formik.touched.email && formik.errors.email}
      <button type="submit">提交</button>
    </form>
  );
}`}</pre>
  </div>
</body>
</html>

浏览器兼容性

以下是React表单相关特性在各主流浏览器中的兼容性情况:

特性 Chrome Firefox Safari Edge 说明
受控组件 全部 全部 全部 全部 React标准API
useRef 全部 全部 全部 全部 React 16.8+
defaultValue 全部 全部 全部 全部 React标准API
File API 30+ 30+ 10+ 12+ FileReader
FormData 40+ 40+ 10+ 12+ 表单数据提交
Constraint Validation 40+ 40+ 10+ 12+ 原生验证API
input type=date 20+ 57+ 14.1+ 12+ 日期输入

注意事项与最佳实践

  • 优先使用受控组件:受控组件让React完全控制表单数据,便于验证和条件渲染

  • 文件输入必须用非受控<input type="file">的value是只读的,只能用ref获取文件

  • 使用defaultValue设置初始值:非受控组件使用defaultValue而非value

  • 表单验证时机:在onBlur时验证(用户离开输入框),而非每次onChange都验证

  • 提取自定义Hook:将表单逻辑提取为useForm等自定义Hook,提高复用性

  • 大型表单使用表单库:字段超过5个的表单建议使用React Hook Form

  • 防止重复提交:提交时禁用按钮或使用isSubmitting状态


代码规范示例

以下是React表单开发中的推荐与不推荐写法对比:

代码示例

// 不推荐:每个字段单独管理状态
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');

// 推荐:使用对象统一管理
const [formData, setFormData] = useState({
  name: '', email: '', phone: ''
});
function handleChange(e) {
  const { name, value } = e.target;
  setFormData(prev => ({ ...prev, [name]: value }));
}

// 不推荐:提交时才验证
function handleSubmit(e) {
  e.preventDefault();
  if (!name) alert('姓名不能为空');
  if (!email) alert('邮箱不能为空');
}

// 推荐:实时验证 + 提交时全量验证
function handleBlur(e) {
  validateField(e.target.name);
}
function handleSubmit(e) {
  e.preventDefault();
  if (validateAll()) { /* 提交 */ }
}

// 不推荐:内联复杂验证逻辑
<input onChange={(e) => {
  const val = e.target.value;
  if (val.length < 6) setError('太短');
  else if (!/[A-Z]/.test(val)) setError('需要大写');
  else if (!/[0-9]/.test(val)) setError('需要数字');
  else setError('');
}} />

// 推荐:提取验证函数
function validatePassword(value) {
  if (value.length < 6) return '太短';
  if (!/[A-Z]/.test(value)) return '需要大写字母';
  if (!/[0-9]/.test(value)) return '需要数字';
  return '';
}

常见问题与解决方案

特性 原生React React Hook Form Formik
包体积 0KB ~8KB ~13KB
渲染性能 取决于实现 优秀(非受控) 一般(受控)
学习曲线
验证方案 手动实现 内置+Schema 内置+Schema
适用场景 简单表单 中大型表单 中大型表单
推荐度 简单场景 推荐 维护项目

常见问题

受控组件输入框无法输入怎么办?

常见原因是设置了value但没有onChange处理函数,或者onChange中没有正确更新状态。确保value={state}onChange={e => setState(e.target.value)}配对使用。

textarea在React中的写法?

React中textarea使用value属性而非children:<textarea value={text} onChange={handleChange} />,而非HTML中的<textarea>文本</textarea>

select的默认选中如何设置?

在受控组件中,通过value属性设置选中项:<select value={selectedValue} onChange={handleChange}>。非受控组件使用defaultValue。

如何处理大量表单字段的性能问题?

使用React Hook Form(非受控方式,减少重渲染),或将大表单拆分为多个子组件,每个子组件管理自己的状态。

表单数据如何提交到服务器?

使用fetch或axios发送数据。注意preventDefault阻止默认提交行为,然后手动发送请求。也可以使用FormData对象处理文件上传。


总结

本教程详细介绍了React表单与HTML的集成。受控组件是React推荐的表单处理方式,通过状态完全控制表单值,便于实时验证和条件渲染。非受控组件通过ref直接访问DOM,适用于文件输入等场景。表单验证应结合onBlur实时验证和提交时全量验证,多步骤表单需要合理管理步骤状态。

对于复杂表单,推荐使用React Hook Form等表单库来简化开发。理解受控与非受控的选择、验证策略和性能优化是构建高质量React表单的关键。下一教程将介绍React样式与HTML的集成。

提示:在实际项目中,建议根据表单复杂度选择合适的方案——简单表单用原生React受控组件,中大型表单优先使用React Hook Form,维护旧项目可考虑Formik。

标签: React表单 受控组件 非受控组件 表单验证 React Hook Form Formik

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 框架集成:05 React事件处理与HT - 详细教程与实战指南 下一篇: 框架集成:07 React样式与HTML - 详细教程与实战指南

poll相关推荐