受控组件与非受控组件
一、受控组件
HTML 表单元素通常会自己维护一套 state,并根据用户的输入进行更新,而如果将 React 的 state 属性和表单元素的值建立依赖关系,再通过 onChange
与 setState()
相结合来更新 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 是个可读的状态。
这是因为 value 被 this.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 受控组件通用钩子
定义:
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;