Skip to main content

受控组件与非受控组件

一、受控组件

HTML 表单元素通常会自己维护一套 state,并根据用户的输入进行更新,而如果将 React 的 state 属性和表单元素的值建立依赖关系,再通过 onChangesetState() 相结合来更新 state 的值,使 React state 成为表单元素的唯一数据源,从而控制着用户输入过程中表单发生的操作,被 React 以这种方式控制取值的表单输入元素就叫做受控组件。

举个例子:

class TestComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
username: 'leophen'
}
}
render() {
return <input name="username" value={this.state.username} />
}
}

当在输入框输入内容时,会发现输入功能被锁定,即 input 是个可读的状态。

这是因为 valuethis.state.username 所控制,当用户输入新内容时,this.state.username 并不会自动更新,也就导致了 input 的内容不会改变。如果想要解除被控制,可以为 input 标签设置 onChange 事件,输入时候触发事件处理函数,在函数内实现 state 的更新,从而使 input 的内容发生改变:

class TestComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
username: 'leophen'
}
}
onChange(e) {
this.setState({
username: e.target.value
})
}
render() {
return (
<input
name="username"
value={this.state.username}
onChange={(e) => this.onChange(e)}
/>
)
}
}

因此,受控组件一般需要初始状态和状态更新事件函数。

有时使用受控组件会很麻烦,需要为数据变化的每种方式都编写事件处理函数,并通过一个 React 组件传递所有的输入 state。可以使用非受控组件, 这是实现输入表单的另一种方式。

如果想寻找包含验证、追踪访问字段以及处理表单提交的完整解决方案,可以使用 Formik,它也是建立在受控组件和管理 state 的基础之上。

二、非受控组件

受控组件的表单数据由 React 进行管理,而非受控组件中,表单数据将交由 DOM 节点来处理,不用为每个状态更新编写事件处理函数。

非受控组件可以使用 ref 来从 DOM 节点中获取表单数据。举个例子:

class NameForm extends React.Component {
constructor(props) {
super(props)
this.handleSubmit = this.handleSubmit.bind(this)
this.inputRef = React.createRef()
}

handleSubmit(event) {
alert('A name was submitted: ' + this.input.current.value)
event.preventDefault()
}

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={this.inputRef} />
</label>
<input type="submit" value="提交" />
</form>
)
}
}

上面的例子中,在输入框输入内容后,点击提交按钮,可以通过 this.inputRef 拿到 input 的 DOM 属性信息,包括用户输入的值,这样就不需要像受控组件一样,单独为每个表单元素维护一个状态。

非受控组件中,经常希望 React 能赋予组件一个初始值,但不去控制后续的更新。可以指定一个 defaultValue 属性,组件挂载后会去更新 defaultValue 的值:

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input defaultValue="Bob" type="text" ref={this.inputRef} />
</label>
<input type="submit" value="Submit" />
</form>
)
}

另外,React 中上传组件 <input type="file" /> 始终是个非受控组件,它的值只能由用户设置,不能通过代码控制。

三、应用场景

大部分情况使用受控组件来实现表单,因为在受控组件中,表单数据由 React 处理,如果使用非受控组件,表单数据就由 DOM 本身处理,控制能力较弱,但代码量少,更加方便快捷。

二者的应用场景如下:

场景使用受控组件使用非受控组件
一次性取值(提交时)
提交时验证
即时现场验证
有条件地禁用提交按钮
强制输入格式
一个数据的多个输入
动态输入

四、React 受控组件通用钩子

定义:

src/hooks/useDefault.ts
import { useState } from 'react';

export interface ChangeHandler<T, P extends any[]> {
// @ts-ignore
(value: T, ...args: P);
}

export default function useDefault<T, P extends any[]>(
value: T | undefined,
defaultValue: T,
onChange: ChangeHandler<T, P> | any,
): [T, ChangeHandler<T, P>] {
// 无论是否受控,都要 useState,因为 Hooks 是无条件的
const [internalValue, setInternalValue] = useState(defaultValue);

const defaultFn = () => {};

// 受控模式
if (typeof value !== 'undefined') {
return [value, onChange || defaultFn];
}

// 非受控模式
return [
internalValue,
(newValue, ...args) => {
setInternalValue(newValue);
if (typeof onChange === 'function') {
onChange(newValue, ...args);
}
},
];
}

使用:

import React from 'react';
import useDefault from '../hooks/useDefault';

interface SwitchProps {
/**
* 开关固定值(受控)
*/
value?: boolean;
/**
* 开关默认值(非受控)
* @default false
*/
defaultValue?: boolean;
/**
* 切换开关时触发
*/
onChange?: (value: boolean) => void;
}

const Switch: React.FC<SwitchProps> = (props) => {
const {
value,
defaultValue = false,
onChange,
...switchProps
} = props;

const [innerValue, setInnerValue] = useDefault(value, defaultValue, onChange);

const handleSwitch = () => {
// 注意这里不需要额外调用 onChange,钩子函数中已经做了处理
setInnerValue(!innerValue)
}

return (
<button
onClick={handleSwitch}
{...switchProps}
>
{innerValue ? '开' : '关'}
</button>
);
};

Switch.displayName = 'Switch';

export default Switch;

点击查看 Vue 受控组件的用法