受控组件与非受控组件:React 表单处理的两种方式

165 阅读5分钟

一、非受控组件(Uncontrolled Component)

1. 定义

非受控组件是指表单元素的值由 DOM 自身管理,React 通过 ref 来获取其值。它更接近传统的 HTML 表单行为。

2. 原理

  • 使用 useRef 创建一个引用(reference)来访问 DOM 元素。
  • 在提交表单时,通过 ref.current.value 获取输入值。
  • 不需要为每个输入设置 onChange 和状态。

3. 代码演示

import React, { useRef } from 'react';

function UncontrolledInput() {
  // 使用 useRef 创建一个对 input 元素的引用
  const inputRef = useRef(null);

  // 表单提交时触发的函数
  const handleSubmit = (e) => {
    e.preventDefault(); // 阻止默认的表单提交行为
    const value = inputRef.current.value; // 通过 ref 获取 DOM 的值
    console.log('提交的值为:', value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        用户名:
        {/* 使用 ref 绑定到 input 元素 */}
        <input type="text" ref={inputRef} />
      </label>
      <button type="submit">提交</button>
    </form>
  );
}

4. 优点

  • 性能更优:没有频繁的状态更新,适用于对性能敏感的场景。
  • 代码简洁:无需为每个字段绑定状态和事件处理函数。
  • 接近原生行为:适合熟悉原生表单处理的开发者。

5. 缺点

  • 缺乏实时控制:无法在输入时立即获取或修改值。
  • 难以实现复杂逻辑:如表单校验、联动、动态更新等。
  • 调试困难:数据不在 React 状态中,调试和追踪较难。
  • 不支持 React 的某些特性:如状态快照、上下文、状态管理库等。

二、受控组件(Controlled Component)

1. 定义

受控组件是指表单元素的值由 React 组件的状态(state)控制,并通过 onChange 事件来更新状态。换句话说,React 是表单状态的唯一数据源。

2. 原理

  • 表单元素的 value 属性绑定到 React 的状态。
  • 每次用户输入都会触发 onChange 事件。
  • 事件处理函数中更新状态,从而更新输入框的值。

3. 代码演示

import React, { useState } from 'react';

function ControlledInput() {
  // 使用 useState 管理输入框的值和错误信息
  const [value, setValue] = useState(''); // 输入框的值
  const [error, setError] = useState(''); // 错误提示信息

  // 输入变化时触发的函数
  const handleChange = (e) => {
    const inputValue = e.target.value;
    setValue(inputValue); // 更新输入框的值

    // 实时校验输入内容长度
    if (inputValue.length < 6) {
      setError('输入内容不能少于6个字符');
    } else {
      setError('');
    }
  };

  // 表单提交时触发的函数
  const handleSubmit = (e) => {
    e.preventDefault(); // 阻止默认的表单提交行为
    console.log('提交的值为:', value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        用户名:
        {/* 输入框的 value 由 state 控制 */}
        <input
          type="text"
          value={value}
          onChange={handleChange} // 每次输入都会触发 handleChange
        />
      </label>
      {/* 如果 error 存在,则显示错误提示 */}
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <button type="submit">提交</button>
    </form>
  );
}

4. 优点

  • 数据同步性高:所有输入数据都保存在 React 的状态中,便于统一管理。
  • 支持实时校验:可以在用户输入时立即进行格式或内容验证。
  • 支持动态更新:输入框的值可以被其他状态或逻辑动态修改。
  • 易于重置表单:只需重置状态即可恢复初始值。
  • 便于集成其他功能:如表单联动、数据回填、表单校验等。

5. 缺点

  • 性能开销:每次输入都会触发 onChange,更新状态,可能带来轻微性能影响(在现代 React 中影响不大)。
  • 代码量较大:需要为每个字段设置状态和事件处理函数,代码量相对较多。
  • 复杂度高:对于大型表单,管理多个状态和校验逻辑可能变得复杂。

三、对比总结

特性受控组件非受控组件
数据来源React 状态DOM 元素
实时响应✅ 支持❌ 不支持
表单校验✅ 简单实现❌ 需手动处理
动态更新✅ 支持❌ 不支持
性能略低(频繁触发 onChange✅ 更轻量
适用场景复杂表单、需校验、交互频繁简单输入、性能优先
代码复杂度较高较低
可维护性✅ 更好❌ 较差

四、适用场景分析

1. 推荐使用受控组件的场景

  • 表单需要实时校验(如用户名长度、邮箱格式等)。
  • 表单字段之间存在联动(如城市随省份变化而变化)。
  • 表单数据需要动态初始化或重置。
  • 使用了状态管理库(如 Redux、MobX、Zustand)。
  • 需要与 React 的其他特性(如 Context、Hooks、组件通信)集成。

2. 推荐使用非受控组件的场景

  • 表单字段简单,仅用于提交数据,无需复杂逻辑。
  • 对性能要求较高,且表单交互较少。
  • 快速原型开发或临时表单。
  • 与第三方库集成时(如某些表单库或富文本编辑器)。

五、选择组件的考虑

1. 表单复杂度决定组件类型

  • 简单输入:如搜索框、留言框,可以使用非受控组件。
  • 复杂表单:如注册、登录、配置表单,推荐使用受控组件。

2. 是否需要实时交互

  • 如果需要在输入时进行校验、提示、联动等操作,使用受控组件。
  • 如果只是提交数据,无需复杂交互,可使用非受控组件。

3. 性能考虑

  • 对于大型表单或频繁更新的输入,可以结合 防抖(debounce)节流(throttle) 技术优化性能。
  • 非受控组件在某些场景下性能更优,但通常差异不大。

六、性能优化建议

1. 使用 useCallback 优化事件处理函数

避免在每次渲染时都创建新的函数,提升性能:

const handleChange = useCallback((e) => {
  setValue(e.target.value);
}, []);

想了解useCallback可以看看:React 性能调优必备:React.memo、useCallback、useMemo 在 React 开发中,性能优化 - 掘金

2. 防抖优化输入处理(适用于受控组件)

import { debounce } from 'lodash-es';

const debouncedChange = debounce((e) => {
  setValue(e.target.value);
}, 300);

<input type="text" onChange={debouncedChange} />

想了解防抖节流可以看看:那些年我们忽略的高频事件,正在拖垮你的页面高频操作下的性能杀手锏!本文详解防抖与节流原理及实战应用,助你写出更高效、更优 - 掘金

3. 使用表单管理库(如 Formik、React Hook Form)

对于大型表单,推荐使用表单管理库,它们封装了受控组件的复杂性,提供了更简洁的 API 和丰富的功能(如校验、错误提示、提交处理等):

pnpm install react-hook-form

示例代码:

import { useForm } from 'react-hook-form';

function LoginForm() {
  const { register, handleSubmit } = useForm();

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('username', { required: true, minLength: 6 })} />
      <button type="submit">登录</button>
    </form>
  );
}

这个可以去官网学习一下:React Hook Form - 高性能、灵活且可扩展的表单库 - React Hook Form 中文