0基础进大厂,第25天——React中的受控组件与非受控组件

92 阅读4分钟

引言:表单处理的两种范式

在React开发中,表单处理是每个开发者都必须掌握的核心技能。React为我们提供了两种处理表单的方式:受控组件(Controlled Components)和非受控组件(Uncontrolled Components)。这两种方式各有优劣,适用于不同场景。

接下来鼠鼠带你深入剖析这两种组件的实现原理、使用场景。

1. 受控组件 - 数据流的完全掌控

基本概念

受控组件是指表单数据完全由React组件状态管理的组件。在这种模式下,表单元素的值由React state控制,任何用户输入都会触发状态更新,从而重新渲染组件。

基本用法

import { useState } from 'react';

function ControlledForm() {
  const [value, setValue] = useState('');

  const handleChange = (e) => {
    setValue(e.target.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('提交的值:', value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text" 
        value={value}
        onChange={handleChange}
      />
      <button type="submit">提交</button>
    </form>
  );
}

关键特性

  1. 单一数据源:表单数据只存在于React state中
  2. 即时验证:每次输入都能立即验证数据有效性
  3. 完全控制:可以精确控制输入过程中的每个状态变化

适用场景

  • 需要即时验证用户输入
  • 表单值需要根据其他状态动态计算
  • 需要禁用/启用提交按钮基于表单状态
  • 强制特定输入格式

2. 非受控组件 - 原生的力量

基本概念

非受控组件是指表单数据由DOM本身处理的组件。在这种模式下,我们使用ref来从DOM节点直接获取表单值,而不是为每个状态更新都编写处理函数。

基本用法

import { useRef } from 'react';

function UncontrolledForm() {
  const inputRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('提交的值:', inputRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text" 
        ref={inputRef}
        defaultValue="初始值"
      />
      <button type="submit">提交</button>
    </form>
  );
}

关键特性

  1. DOM驱动:表单数据存储在DOM中而非React state
  2. 性能优势:避免频繁的状态更新和重新渲染
  3. 简单直接:代码量少,适合简单表单

适用场景

  • 简单表单,不需要即时验证
  • 大型表单,性能是关键考量
  • 需要与第三方库集成
  • 文件上传等特殊表单元素

3. 深度对比:选择正确的武器

特性受控组件非受控组件
数据存储位置React stateDOM节点
数据获取方式从state直接读取通过ref访问DOM
表单验证时机每次输入变化提交时
性能影响每次输入都触发重新渲染无额外渲染
代码复杂度较高较低
与React生态集成完全集成可能需要额外处理
文件上传支持不支持支持

4. 案例:表单验证的不同实现

受控组件实现即时验证

function ControlledValidation() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

  const handleChange = (e) => {
    const value = e.target.value;
    setEmail(value);
    
    // 即时验证邮箱格式
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      setError('请输入有效的邮箱地址');
    } else {
      setError('');
    }
  };

  return (
    <div>
      <input 
        type="email"
        value={email}
        onChange={handleChange}
      />
      {error && <div style={{color: 'red'}}>{error}</div>}
    </div>
  );
}

非受控组件实现提交时验证

function UncontrolledValidation() {
  const emailRef = useRef(null);
  const [error, setError] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    const email = emailRef.current.value;
    
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      setError('请输入有效的邮箱地址');
      return;
    }
    
    // 提交逻辑...
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="email"
        ref={emailRef}
        defaultValue=""
      />
      <button type="submit">提交</button>
      {error && <div style={{color: 'red'}}>{error}</div>}
    </form>
  );
}

5. 混合使用:

在实际开发中,我们经常需要混合使用这两种方式:

function MixedForm() {
  const [name, setName] = useState('');
  const fileInputRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData();
    formData.append('name', name);
    formData.append('file', fileInputRef.current.files[0]);
    
    // 提交逻辑...
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      
      <input
        type="file"
        ref={fileInputRef}
      />
      
      <button type="submit">提交</button>
    </form>
  );
}

6. 性能优化的一些小技巧

  1. 防抖处理:对受控组件的频繁更新进行防抖

    const [value, setValue] = useState('');
    const debouncedValue = useDebounce(value, 500);
    
    // 使用debouncedValue进行耗时操作
    
  2. 批量更新:对多个相关字段使用单个state对象

    const [form, setForm] = useState({
      username: '',
      password: '',
      email: ''
    });
    
    const handleChange = (e) => {
      const { name, value } = e.target;
      setForm(prev => ({ ...prev, [name]: value }));
    };
    
  3. 避免不必要的渲染:使用React.memo优化子组件

7. 常见问题

Q: 为什么我的受控输入感觉很卡? A: 可能是由于在onChange中执行了耗时操作,考虑使用防抖或优化处理逻辑。

Q: 如何重置非受控表单? A: 可以通过修改key强制重新渲染组件,或者使用DOM API直接重置表单。

Q: 文件上传必须用非受控组件吗? A: 是的,文件输入是只读的,必须使用非受控方式处理。

8. 总结:

  • 选择受控组件当:

    • 需要即时反馈和验证
    • 表单状态影响UI的其他部分
    • 需要强制特定的输入格式
  • 选择非受控组件当:

    • 表单非常简单
    • 性能是关键考量
    • 需要处理文件上传
    • 与大量非React代码集成

React哲学鼓励我们"提升状态",但这并不意味着所有状态都需要提升到React中管理。根据实际需求选择最合适的方案,才是优秀开发者的标志。

在大多数现代React应用中,受控组件是更常见的选择,因为它提供了更好的可预测性和控制力。