你可能不知道 react 组件中受控和非受控的秘密!

前言

在 React.js 中,涉及到处理表单的所有组件,你都不得不先弄清楚一个问题,什么是表单组件的受控和非受控模式!

以下是我的 headless 组件库新增的 Checkbox 组件(这个组件主要参考了字节的 arco-design、阿里的 ant-design 和国外的 shadcn/ui 源码),网站也有详细的组件教程,欢迎点赞交流。

关于受控和非受控的应用具体案例,可以看我的 headless 组件库的 checkbox

其中两个核心概念是 受控组件(Controlled Components) 和 非受控组件(Uncontrolled Components)。它们决定了表单数据在 React 组件中是如何被管理的。简单来说:

  • 受控组件 完全依赖 React 的 state 来存储和更新表单数据。

  • 非受控组件 则依赖原生 DOM 自身来管理表单数据。

本文将带你理解这两类组件的区别、实现方式,并提供在实际应用中使用它们的最佳实践。

什么是受控组件?

受控组件指由 React state 完全管理 的表单元素(如 input、textarea、select 等)。 也就是说,该表单元素的值完全由 React state 决定,React 成为表单数据的 唯一数据源。

通过使用 state 控制表单,你可以:

  • 更精确地控制用户输入行为

  • 轻松执行校验逻辑

  • 格式化输入等等

我们举个例子:

javascript 复制代码
import React, { useState } from 'react';

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

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

  const handleSubmit = (event) => {
    event.preventDefault();
    alert('A name was submitted: ' + value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input type="text" value={value} onChange={handleChange} />
      </label>
      <button type="submit">Submit</button>
    </form>
  );
}

export default ControlledComponent;

在这个示例中:

  • value 用来保存输入框的当前值。

  • handleChange 在用户输入时更新该 state。

  • handleSubmit 在提交时使用 state 中的值。

我们小小总结一下:受控组件的核心在于:表单数据完全由 React state 管理。

使用受控组件的优势

一般情况下,复杂g场景都会使用受控模式,因为两个很明显的好处:

  • 更容易与复杂 UI 库集成
  • 更容易实现表单校验

与常见组件库集成

国内主流组件库的表单,基本都是来自于 ant-design 的 Form 组件逻辑, From 组件说白了,就是一个中介者模式,也就是说,Form 组件内部维护了一个收集表单数据的 store,当表单数据修改的时候, From 中的 store 会同步修改,并且刷新视图。

所以这里我们必须要知道什么时候数据被修改了,这就是受控组件的优势,onChange 事件触发,就意味着数据很可能被修改了

更容易实现表单校验

正因为能够显式的收集表单数据的变化(第三方 UI 库),才能做到在提交表单的时候,我们能够有机会来统一校验值是否符合业务要求。

或者在填写表单的时候,就显示错误信息。

举例:

jsx 复制代码
import React, { useState } from 'react';

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

  const handleChange = (event) => {
    const value = event.target.value;
    setEmail(value);

    if (!value.includes('@')) {
      setError('Invalid email address');
    } else {
      setError('');
    }
  };

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

上述例子会在用户输入的每一步进行校验,并提供即时反馈。

最后我们可以再来看两个封装一个受控组件的案例,体会其用法:

文本输入框

jsx 复制代码
import React, { useState } from 'react';

function TextInput() {
  const [text, setText] = useState('');

  const handleChange = (event) => {
    setText(event.target.value);
  };

  return (
    <div>
      <label>
        Text:
        <input type="text" value={text} onChange={handleChange} />
      </label>
      <p>Entered Text: {text}</p>
    </div>
  );
}

export default TextInput;

复选框(Checkbox)

javascript 复制代码
import React, { useState } from 'react';

function Checkbox() {
  const [isChecked, setIsChecked] = useState(false);

  const handleChange = (event) => {
    setIsChecked(event.target.checked);
  };

  return (
    <div>
      <label>
        Accept Terms:
        <input type="checkbox" checked={isChecked} onChange={handleChange} />
      </label>
      <p>Checked: {isChecked ? 'Yes' : 'No'}</p>
    </div>
  );
}

export default Checkbox;

什么是非受控组件

非受控组件的状态由 DOM 自己管理,而不是 React state。 这种方式通常依赖 ref 来读取 DOM 中的值。我们来举个例子:

jsx 复制代码
import React, { Component } from 'react';

class UncontrolledComponent extends Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }

  handleSubmit = () => {
    console.log(this.inputRef.current.value);
  }

  render() {
    return (
      <div>
        <input 
          type="text"
          ref={this.inputRef}
        />
        <button onClick={this.handleSubmit}>Submit</button>
      </div>
    );
  }
}

从上面我们可以看到非受控组件的特点:

  1. 内部管理状态

表单的状态存储在 DOM 中,而非 React state。

  1. 直接访问 DOM

通过 ref.current.value 获取值。

  1. 更简单

适用于无需实时校验的场景

什么时候适合非受控组件

简单单表单

适用情况包括:

  • 表单结构简单,字段少

  • 不需要复杂校验或联动逻辑

  • 不希望 state 频繁更新导致 re-render

注意

这里特别注意一个优点,就是不希望 state 频繁更新导致 re-render,这让非受控组件的性能非常高。

国外流行的 React-hook-form 的性能高的原因,就是主要以非受控为主。

其实 ant-design 内部为了避免 setState 每次全量刷新表单造成性能问题,主要的方式是采用了发布订阅模式,把所有表单都订阅到了 store 里,当一个表单发生变化时,只刷新这个表单的 state,从而减少了性能损耗。

组件库如何合并受控和非受控状态

我们的组件库使用了 useMergeValue hook, 它可以用来合并受控和非受控模式.

这个 hook 大家可以复制下来,在写表单组件的时候,非常好用(在各种 ui 库中广泛存在)。首先我们先看一下简单用法:

假设我们在处理一个 radio 组件,radio 组件默认是不被选中的,所以 useMergeValue 的默认值,也就是第一个参数是 false, 然后 value 表示是否是受控模式,如果是,外界就传了 value,代表外界接管了 radio 的 state 变化。当然非受控模式一般也支持传一个 defaultValue,所以 defaultValue 代表了外界采用的是非受控模式:

javascript 复制代码
  const [checked, setChecked] = useMergeValue(false, {
    value: props.checked,
    defaultValue: props.defaultChecked,
  });

那么我们合并受控和非受控的思路是什么呢?

完整代码如下,先别急,后面会有详细解释:

javascript 复制代码
'use client';

import React, { useState, useEffect, useRef } from 'react';
import { isUndefined } from '../utils';
import { usePrevious } from './use-previous';

export function useMergeValue<T>(
  defaultStateValue: T,
  props?: {
    defaultValue?: T;
    value?: T;
  },
): [T, React.Dispatch<React.SetStateAction<T>>, T] {
  const { defaultValue, value } = props || {};
  const firstRenderRef = useRef(true);
  const prevPropsValue = usePrevious(props?.value);

  const [stateValue, setStateValue] = useState<T>(
    !isUndefined(value) ? value : !isUndefined(defaultValue) ? defaultValue : defaultStateValue,
  );

  // 受控转为非受控的时候,需要做转换处理
  useEffect(() => {
    if (firstRenderRef.current) {
      firstRenderRef.current = false;
      return;
    }
    if (value === undefined && prevPropsValue !== value) {
      setStateValue(value);
    }
  }, [value]);

  const mergedValue = isUndefined(value) ? stateValue : value;

  return [mergedValue, setStateValue, stateValue];
}

我们逐条拆解:

1. 初始化:判断当前是受控还是非受控

javascript 复制代码
const [stateValue, setStateValue] = useState(
  value ?? defaultValue ?? defaultStateValue
);

顺序解释:

  1. 如果用户传了 value → 是受控组件
  2. 否则如果传了 defaultValue → 是非受控组件
  3. 否则 → 使用组件内部默认值 defaultStateValue

👉 这一步决定了初始模式是 Controlled 还是 Uncontrolled。

2. 运行时:保持跟随受控 value 的变化

javascript 复制代码
const mergedValue = isUndefined(value) ? stateValue : value;

意思是:

  • 如果外面传了 value → 受控,永远以外界 value 为准
  • 如果外界没传 value → 非受控,使用内部 state

👉 组件内部不需要关心模式切换,只看 mergedValue 就够了。

3. 特殊处理:当"受控 → 非受控"时,需要同步内部 state

这是最难理解的部分,也是很多 UI 库都踩过的坑:

javascript 复制代码
useEffect(() => {
  if (value === undefined && prevPropsValue !== value) {
    setStateValue(value);
  }
}, [value]);

说明:

  • 如果上一次是受控(有 value)

  • 现在变成非受控(value = undefined)

  • 那么内部 state 应该更新为 value(undefined)

    • 这等价于"清空或重置内部状态"

也就是切换受控和非受控的状态

例如:

javascript 复制代码
<Checkbox checked={true} />   // 受控,永远选中
// 某个时刻用户把 checked 用 undefined 覆盖掉,比如执行了 reset
<Checkbox checked={undefined} />  // 变成非受控

如果不特殊处理,组件内部 state 还是旧的 true,就会产生 UI 错乱。

欢迎加入交流群

相关推荐
火车叼位38 分钟前
ast-grep:结构化搜索与重构利器
前端
over69742 分钟前
深入理解 JavaScript 原型链与继承机制:从 instanceof 到多种继承模式
前端·javascript·面试
烂不烂问厨房1 小时前
前端实现docx与pdf预览
前端·javascript·pdf
GDAL1 小时前
Vue3 Computed 深入讲解(聚焦 Vue3 特性)
前端·javascript·vue.js
Moment1 小时前
半年时间使用 Tiptap 开发一个和飞书差不多效果的协同文档 😍😍😍
前端·javascript·后端
前端加油站1 小时前
记一个前端导出excel受限问题
前端·javascript
da_vinci_x1 小时前
PS 生成式扩展:从 iPad 到带鱼屏,游戏立绘“全终端”适配流
前端·人工智能·游戏·ui·aigc·技术美术·游戏美术
一壶纱1 小时前
uni-app 中配置 UnoCSS
前端·vue.js
步履不停_1 小时前
告别输入密码!打造基于 VS Code 的极致远程开发工作流
前端·visual studio code