Skip to content
第 7 章 前端 ⏱ 14 分钟阅读

第 7 章:表单 ​

学习目标 ​

  • 理解受控组件 vs 非受控组件
  • 掌握 input/textarea/select/checkbox 的写法
  • 用 FormData + onSubmit 处理表单提交
  • 避开表单的 5 个常见坑

一、受控 vs 非受控 ​

受控组件(推荐):表单值由 React state 控制,输入触发 onChange 更新 state。

tsx
function Form() {
  const [name, setName] = useState('');

  return (
    <input
      value={name}
      onChange={e => setName(e.target.value)}
    />
  );
}

非受控组件:用 ref 直接读 DOM 值,React 不管。

tsx
function Form() {
  const inputRef = useRef<HTMLInputElement>(null);

  function handleSubmit() {
    console.log(inputRef.current?.value);     // ⚠️ 直接读 DOM
  }

  return <input ref={inputRef} />;
}

⚠️ 坑 1:受控组件的 input 必须有 value 和 onChange,只设 value 会只读,输入框敲不动。

二、各种表单元素 ​

text input ​

tsx
const [username, setUsername] = useState('');
<input value={username} onChange={e => setUsername(e.target.value)} />

textarea ​

tsx
const [bio, setBio] = useState('');
<textarea value={bio} onChange={e => setBio(e.target.value)} />

select(单选) ​

tsx
const [city, setCity] = useState('bj');

<select value={city} onChange={e => setCity(e.target.value)}>
  <option value="bj">北京</option>
  <option value="sh">上海</option>
  <option value="sz">深圳</option>
</select>

⚠️ 坑 2:React 用 value 选中,不是 selected 属性。

checkbox ​

tsx
const [agree, setAgree] = useState(false);

<input
  type="checkbox"
  checked={agree}
  onChange={e => setAgree(e.target.checked)}
/>

radio ​

tsx
const [gender, setGender] = useState('m');

<label>
  <input type="radio" value="m" checked={gender === 'm'} onChange={e => setGender(e.target.value)} />
  男
</label>
<label>
  <input type="radio" value="f" checked={gender === 'f'} onChange={e => setGender(e.target.value)} />
  女
</label>

file input(必须非受控) ​

tsx
<input type="file" ref={fileRef} />
function handle() { console.log(fileRef.current?.files); }

⚠️ 坑 3:type="file" 必须用 ref,因为文件值不能由 JS 设置(浏览器安全限制)。

三、统一表单 state(对象) ​

tsx
interface Form {
  username: string;
  password: string;
  remember: boolean;
  gender: 'm' | 'f';
  city: string;
}

function LoginForm() {
  const [form, setForm] = useState<Form>({
    username: '',
    password: '',
    remember: false,
    gender: 'm',
    city: 'bj',
  });

  function update<K extends keyof Form>(key: K, value: Form[K]) {
    setForm(prev => ({ ...prev, [key]: value }));
  }

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    console.log('提交', form);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={form.username} onChange={e => update('username', e.target.value)} />
      <input type="password" value={form.password} onChange={e => update('password', e.target.value)} />
      <input type="checkbox" checked={form.remember} onChange={e => update('remember', e.target.checked)} />
      <button type="submit">登录</button>
    </form>
  );
}

四、表单验证 ​

tsx
const [errors, setErrors] = useState<Record<string, string>>({});

function validate(): boolean {
  const e: Record<string, string> = {};
  if (!form.username) e.username = '用户名必填';
  if (form.password.length < 6) e.password = '密码至少 6 位';
  setErrors(e);
  return Object.keys(e).length === 0;
}

function handleSubmit(e: React.FormEvent) {
  e.preventDefault();
  if (!validate()) return;
  // 提交
}

五、非受控 + FormData ​

适合大表单或简单场景:

tsx
function Upload() {
  const formRef = useRef<HTMLFormElement>(null);

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    const data = new FormData(formRef.current!);     // ⚠️ FormData 自动收集
    fetch('/api/upload', { method: 'POST', body: data });
  }

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <input name="title" />
      <input name="file" type="file" />
      <button>上传</button>
    </form>
  );
}

⚠️ 坑 4:非受控输入必须加 name 属性,FormData 通过 name 收集值。

六、第三方库(进阶) ​

复杂表单用 react-hook-form + zod,代码量减半:

tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email(),
  age: z.number().min(18),
});

function MyForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit(data => console.log(data))}>
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}
    </form>
  );
}

七、本章小结 ​

要点关键
受控组件value + onChange,输入框跟着 state 走
非受控组件ref 直接读 DOM,文件 input 必须非受控
select用 value 不用 selected
checkbox用 checked + e.target.checked
提交onSubmit + e.preventDefault()
FormData非受控表单自动收集,需 name 属性
复杂表单推荐 react-hook-form + zod

动手练习 ​

  1. 登录表单:用户名 + 密码 + 记住我,提交时打印对象
  2. 注册表单:加邮箱、城市下拉、性别单选、协议勾选,统一存到 state 对象
  3. 校验:用户名为空报「必填」,密码小于 6 位报「至少 6 位」
  4. 找坑:以下代码为什么敲不进去?
    tsx
    <input value="固定值" />

下一章:第 8 章:生命周期 →

本站基于 VitePress 构建 · 由 StackHub 团队维护