深入理解React中的受控组件与非受控组件

193 阅读8分钟

前言

在React开发中,表单处理是一个常见且重要的任务。React提供了两种主要方式来管理表单数据:受控组件(Controlled Components)和非受控组件(Uncontrolled Components)。本文将深入探讨这两种方式的区别、使用场景以及最佳实践。


什么是受控组件?

受控组件是指表单元素的值由React组件的state控制,并通过事件处理函数同步更新的组件。

PS:想象一下,受控组件就像一个特别听话的小朋友,它的每一个动作都在你的掌控之中。在React中,受控组件就是表单元素的值完全由你来管理的组件。

受控组件的特点

  1. 表单数据由React组件管理:组件的state成为表单元素的"唯一数据源"
  2. 数据流是双向的:state更新表单值,用户输入更新state
  3. 需要为每个表单元素编写事件处理函数(通常是onChange)

受控组件示例

来看个简单例子

// 定义一个受控输入组件,接收onSubmit函数作为props
function ControlledInput({ onSubmit }) {
  // 使用useState钩子创建状态value和更新函数setValue,初始值为空字符串
  const [value, setValue] = useState('');
  
  // 表单提交处理函数
  const handleSubmit = (e) => {
    e.preventDefault(); // 阻止表单默认提交行为
    onSubmit(value);   // 调用父组件传递的onSubmit函数,并将当前输入值作为参数传递
  }
  
  // 输入框变化处理函数
  const handleChange = (e) => {
    e.preventDefault(); // 阻止默认行为(虽然在这里不是必须的)
    // 实时更新value状态为输入框的当前值
    setValue(e.target.value);
  }

  // 返回渲染的表单
  return (
    <form onSubmit={handleSubmit}>
      {/* 表单标签 */}
      <label htmlFor="controlled-input">受控组件</label>
      {/* 受控输入框 
          value属性绑定到组件状态value
          onChange事件绑定到handleChange处理函数
          required表示必填项 */}
      <input 
        type="text" 
        value={value}
        onChange={handleChange}
        required
      />
      {/* 提交按钮 */}
      <input type="submit" value="提交" />
    </form>
  )
}
// 主应用组件
function App() {
  // 定义表单提交处理函数
  const handleSubmit = (value) => {
    // 当ControlledInput提交时,打印接收到的值
    console.log(value, '??????????');
  }

  // 渲染ControlledInput组件,并传入handleSubmit作为onSubmit prop
  return (
    <>
      <ControlledInput onSubmit={handleSubmit}/>
    </>
  )
}

这是一个典型的受控组件实现,输入框的值完全由React状态控制

  1. 组件内部维护一个value状态,与输入框的value属性绑定
  2. 当用户输入时,onChange事件触发handleChange函数,更新value状态
  3. 表单提交时,调用父组件传递的onSubmit回调函数,并将当前输入值value传递出去
  4. App组件定义了一个handleSubmit函数,用于处理子组件ControlledInput提交的数据
  5. handleSubmit函数作为prop(自定义事件)传递给ControlledInput组件
  6. ControlledInput中的表单提交时,会调用这个函数并传入输入值
  7. 当前实现只是简单地将值打印到控制台,实际应用中可能会进行数据处理、API调用等操作

受控组件的优点

  1. 即时验证:可以在用户输入时实时验证数据
  2. 有条件地禁用提交按钮:根据表单数据有效性控制按钮状态
  3. 强制输入格式:可以强制特定的输入格式
  4. 单一数据源:表单数据与组件state保持同步

什么是非受控组件?

非受控组件是指表单数据由DOM本身处理,而不是由React组件控制的组件。你可以使用ref来从DOM节点获取表单值。

PS:非受控组件就像个自主的小朋友,它自己管理自己的事情,你只需要在需要的时候问问它的状态就行了。在React中,我们通过ref来获取它的值。

非受控组件的特点

  1. 表单数据由DOM管理:表单元素保持自己的状态
  2. 数据流是单向的:需要通过ref来获取DOM节点的值
  3. 更接近传统的HTML表单:不需要为每个更新编写事件处理程序

非受控组件示例

// 定义一个非受控输入组件
function UncontrollerInput() {
  // 表单提交处理函数
  const handleSubmit = (e) => {
    e.preventDefault(); // 阻止表单默认提交行为
    // 通过ref获取input DOM节点的当前值
    const value = inputRef.current.value;
    // 打印输入的值到控制台
    console.log(value, '????');
  }
  
  // 使用useRef钩子创建一个ref对象,初始值为null
  const inputRef = useRef(null);

  // 返回渲染的表单
  return (
    <form onSubmit={handleSubmit}>
      {/* 表单标签,关联到输入框 */}
      <label htmlFor="uncontrolled-input">非受控组件</label>
      {/* 非受控输入框 
          使用ref属性绑定到inputRef
          没有value和onChange属性,值由DOM自身管理 */}
      <input 
        type="text" 
        id="uncontrolled-input"
        ref={inputRef}
      />
      {/* 提交按钮 */}
      <input type="submit" value="提交" />
    </form>
  )
}
// 主应用组件
function App() {
  // 定义表单提交处理函数
  const handleSubmit = (value) => {
    // 打印接收到的值(虽然当前代码中这个函数没有被使用)
    console.log(value, '??????????');
  }

  // 渲染UncontrollerInput组件
  // 注意:虽然传入了onSubmit prop,但子组件并未使用
  return (
    <>
      <UncontrollerInput onSubmit={handleSubmit}/>
    </>
  )
}
  1. 用户在输入框中输入内容(由DOM直接管理值的变化)
  2. 用户点击提交按钮
  3. 触发handleSubmit函数,阻止默认提交行为
  4. 通过inputRef.current.value获取输入框的当前值
  5. 将值打印到控制台

代码特点

  1. 非受控实现:输入框值由DOM管理,不经过React状态
  2. ref使用:通过useRef获取DOM节点引用
  3. 简单直接:相比受控组件代码更简洁
  4. 独立处理:表单提交处理完全在组件内部完成

非受控组件的优点

  1. 代码更简单:不需要为每个表单元素编写事件处理程序
  2. 性能更好:对于大型表单,避免了频繁的重新渲染
  3. 与第三方库集成更方便:某些第三方表单库可能更适合非受控方式
  4. 更接近原生HTML:对于简单的表单需求更直观

受控组件 vs 非受控组件

特性受控组件非受控组件
数据管理React组件state管理DOM管理
值控制通过value prop通过defaultValue prop
数据获取直接从state读取需要通过ref获取DOM值
表单验证实时验证提交时验证
性能每次输入都会触发重新渲染性能更好
代码复杂度较高较低
适用场景复杂表单、需要即时反馈简单表单、性能敏感场景

如何选择?

选择受控组件还是非受控组件取决于你的具体需求:

使用受控组件的情况

  1. 需要实时验证用户输入
  2. 需要根据表单输入有条件地禁用提交按钮
  3. 需要强制特定的输入格式
  4. 表单数据需要与多个UI部分同步

使用非受控组件的情况

  1. 表单非常简单,不需要即时验证
  2. 性能是关键考虑因素(如表单非常大)
  3. 需要与第三方库集成
  4. 只需要在提交时获取表单值

最佳实践

  1. 优先考虑受控组件:React官方推荐在大多数情况下使用受控组件,因为它们提供了更好的控制和灵活性。
  2. 对于文件输入始终使用非受控:文件输入(<input type="file" />)始终是非受控的,因为它的值只能由用户设置,不能通过编程方式设置。
  3. 大型表单考虑性能:如果表单非常大且性能成为问题,可以考虑使用非受控组件或优化受控组件的实现。
  4. 混合使用:在某些情况下,可以混合使用两种方式。例如,主表单使用受控组件,而某些特定字段使用非受控组件。
  5. 使用表单库:对于复杂的表单需求,可以考虑使用专门的表单库如Formik或React Hook Form,它们通常提供了两种模式的优化实现。

常见问题解答

Q: 为什么我的受控组件输入无法编辑?

A: 这可能是因为你将value prop绑定到了state,但没有提供onChange处理程序,或者state没有被正确更新。确保你同时提供了value和onChange。

Q: 什么时候应该使用defaultValue而不是value?

A: 当使用非受控组件时,使用defaultValue来设置初始值。对于受控组件,使用value并通过state管理。

Q: 非受控组件是否违背了React的单向数据流原则?

A: 不完全是这样。非受控组件仍然遵循React的渲染流程,只是将表单数据的存储委托给了DOM。你仍然需要通过ref来"拉取"值,而不是DOM"推送"值到React。

Q: 如何在非受控组件中重置表单?

A: 你可以使用key prop来强制重新创建组件,或者直接操作DOM元素将值设置为空(不推荐直接操作DOM)。


结论

受控组件和非受控组件各有优缺点,理解它们的区别和适用场景对于构建高效、可维护的React表单至关重要。在大多数情况下,受控组件是更好的选择,因为它提供了更多的控制和灵活性。然而,在性能关键或简单表单场景中,非受控组件可能更合适。

无论选择哪种方式,重要的是保持一致性和可维护性。随着React生态系统的不断发展,也有许多优秀的表单库可以简化这两种模式的使用,根据项目需求选择合适的工具才是关键。