目录

第十章:表单处理

10.1 受控组件

HTML 中,表单元素如 input、textarea、select 通常自己维护 state。在 React 中,可变状态通常保存在组件的 state 中,只能用 setState 更新。

输入框

function NameForm() {
  const [value, setValue] = useState('');
 
  const handleChange = (e) => {
    setValue(e.target.value);
  };
 
  const handleSubmit = (e) => {
    e.preventDefault();
    alert('提交的名字: ' + value);
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <label>
        名字:
        <input
          type="text"
          value={value}
          onChange={handleChange}
        />
      </label>
      <input type="submit" value="提交" />
    </form>;
  );
}

文本域

function EssayForm() {
  const [value, setValue] = useState('请撰写关于你喜欢的事物的文章');
 
  const handleChange = (e) => {
    setValue(e.target.value);
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <label>
        文章:
        <textarea value={value} onChange={handleChange} />
      </label>
    </form>;
  );
}

下拉选择

function FlavorForm() {
  const [value, setValue] = useState('coconut');
 
  const handleChange = (e) => {
    setValue(e.target.value);
  };
 
  const handleSubmit = (e) => {
    e.preventDefault();
    alert('你喜欢的风味是: ' + value);
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <label>
        选择你喜欢的风味:
        <select value={value} onChange={handleChange}>
          <option value="grapefruit">葡萄柚</option>
          <option value="lime">酸橙</option>
          <option value="coconut">椰子</option>
          <option value="mango">芒果</option>
        </select>
      </label>
      <input type="submit" value="提交" />
    </form>;
  );
}

多选

function MultipleSelect() {
  const [values, setValues] = useState(['coconut']);
 
  const handleChange = (e) => {
    const options = e.target.options;
    const selected = [];
    for (let i = 0; i < options.length; i++) {
      if (options[i].selected) {
        selected.push(options[i].value);
      }
    }
    setValues(selected);
  };
 
  return (
    <select multiple={true} value={values} onChange={handleChange}>
      <option value="grapefruit">葡萄柚</option>
      <option value="lime">酸橙</option>
      <option value="coconut">椰子</option>
      <option value="mango">芒果</option>
    </select>;
  );
}

10.2 处理多个输入

function ReservationForm() {
  const [state, setState] = useState({
    isGoing: true,
    numberOfGuests: 2
  });
 
  const handleChange = (e) => {
    const { name, type, checked, value } = e.target;
    setState({
      ...state,
      [name]: type === 'checkbox' ? checked : value
    });
  };
 
  return (
    <form>
      <label>
        参与:
        <input
          name="isGoing"
          type="checkbox"
          checked={state.isGoing}
          onChange={handleChange}
        />
      </label>
      <br />
      <label>
        来宾人数:
        <input
          name="numberOfGuests"
          type="number"
          value={state.numberOfGuests}
          onChange={handleChange}
        />
      </label>
    </form>;
  );
}

10.3 非受控组件

非受控组件将表单数据存储在 DOM 中,而不是组件 state。

function NameForm() {
  const inputRef = useRef(null);
 
  const handleSubmit = (e) => {
    e.preventDefault();
    alert('Name: ' + inputRef.current.value);
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <label>
        名字:
        <input type="text" ref={inputRef} />
      </label>
      <button type="submit">提交</button>
    </form>;
  );
}

默认值

function Form() {
  return (
    <form>
      <input defaultValue="Bob" type="text" ref={inputRef} />
      <select defaultValue="B" ref={selectRef}>
        <option value="A">选项 A</option>
        <option value="B">选项 B</option>
      </select>
    </form>;
  );
}

文件输入

文件 input 始终是非受控组件,因为它的值只能由用户设置。

function FileInput() {
  const fileInput = useRef(null);
 
  const handleSubmit = (e) => {
    e.preventDefault();
    const file = fileInput.current.files[0];
    alert(`Selected file - ${file.name}`);
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <label>
        上传文件:
        <input type="file" ref={fileInput} />
      </label>
      <button type="submit">提交</button>
    </form>;
  );
}

10.4 表单验证

即时验证

function ValidationForm() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');
 
  const validateEmail = (value) => {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(value);
  };
 
  const handleChange = (e) => {
    const value = e.target.value;
    setEmail(value);
 
    if (value && !validateEmail(value)) {
      setError('请输入有效的邮箱地址');
    } else {
      setError('');
    }
  };
 
  return (
    <form>
      <input
        type="email"
        value={email}
        onChange={handleChange}
        className={error ? 'error' : ''}
      />
      {error && <span className="error-message">{error}</span>}
    </form>;
  );
}

提交时验证

function FormWithValidation() {
  const [values, setValues] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState({});
 
  const validate = () => {
    const newErrors = {};
 
    if (!values.email) {
      newErrors.email = '邮箱必填';
    } else if (!/\S+@\S+\.\S+/.test(values.email)) {
      newErrors.email = '邮箱格式不正确';
    }
 
    if (!values.password) {
      newErrors.password = '密码必填';
    } else if (values.password.length < 6) {
      newErrors.password = '密码至少 6 位';
    }
 
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
 
  const handleSubmit = (e) => {
    e.preventDefault();
    if (validate()) {
      // 提交表单
    }
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={values.email}
        onChange={(e) => setValues({ ...values, email: e.target.value })}
      />
      {errors.email && <span>{errors.email}</span>}
 
      <input
        type="password"
        value={values.password}
        onChange={(e) => setValues({ ...values, password: e.target.value })}
      />
      {errors.password && <span>{errors.password}</span>}
 
      <button type="submit">提交</button>
    </form>;
  );
}

10.5 使用表单库

React Hook Form

npm install react-hook-form
import { useForm } from 'react-hook-form';
 
function HookFormExample() {
  const { register, handleSubmit, formState: { errors } } = useForm();
 
  const onSubmit = data => console.log(data);
 
  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">提交</button>
    </form>;
  );
}

Formik

npm install formik yup
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
 
const schema = Yup.object({
  email: Yup.string().email('邮箱格式不正确').required('邮箱必填'),
  password: Yup.string().min(6, '密码至少 6 位').required('密码必填')
});
 
function FormikExample() {
  return (
    <Formik
      initialValues={{ email: '', password: '' }}
      validationSchema={schema}
      onSubmit={values => console.log(values)}
    >
      {<Form>
        <Field name="email" type="email" />
        <ErrorMessage name="email" />
 
        <Field name="password" type="password" />
        <ErrorMessage name="password" />
 
        <button type="submit">提交</button>
      </Form>}
    </Formik>;
  );
}

10.6 总结

本章详细介绍了表单处理:

表单是 Web 应用的核心,掌握这些技巧可以大大提高开发效率。