受控表单元素和非受控表单元素
在html中,表单中可以使用入input、select等类型的数据输入控件,这些控件会自己保持自己的状态,,并根据用户输入更新状态。
在react中,数据通常保存在组件的state中,并且通过对应的set方法设置。
通过React状态控制输入的表单元素是受控的。
相反,由组件自己管理自己的状态是非受控的。
受控的表单元素
下面的表单元素,自己维护了自己的状态,name的值,当点击提交的时候,会将数据提交给web服务器。
<form action="" method="get" class="form-example">
<div class="form-example">
<label for="name">Enter your name: </label>
<input type="text" name="name" id="name" required />
</div>
<div class="form-example">
<input type="submit" value="Subscribe!" />
</div>
</form>
类似的,在react中,表单控件也会管理好自己的数据,你可以通过ref来收集数据,最后在需要使用数据(比如提交)的时候,从实例变量上获取到数据。
非受控可以在UI交互逻辑简单,在简单的表单数据收集提交的场景被应用。
class Form extends Component {
handleSubmitClick = () => {
const name = this._name.value;
// do something with `name`
};
render() {
return (
<div>
<input type="text" ref={(input) => (this._name = input)} />
<button onClick={this.handleSubmitClick}>Sign up</button>
</div>
);
}
}
接着,让他变成受控的。
首先需要在组件中声明一个状态,用于维护输入控件的当前值。
然后在输入元素上需要有一个属性(input元素对应的是value)用来接受他的当前值,以及一个回调函数(onchange)用来修改状态。
当用户在输入一个字符时,handleNameChange被调用,将输入元素的当前值设置到状态中,状态改变后,组件使用新的value值重新渲染。
因此,react状态中始终维护了表单的当前值。
受控的好处是:
- 能即时的收集到所有的表单状态
- 能自定义的处理输入校验
- 可以方便的将数据同步到UI上。
class Form extends Component {
constructor() {
super();
this.state = {
name: "",
};
}
handleNameChange = (event) => {
this.setState({ name: event.target.value });
};
render() {
return (
<div>
<input type="text" value={this.state.name} onChange={this.handleNameChange} />
</div>
);
}
}
受控组件和非受控组件
受控组件和非受控组件的区别
非受控组件是指组件状态封闭在组件内,不受外部环境的影响。
受控组件指可以接受外部环境传入的value值,改变组件内部状态的组件。
对于组件库的一些组件,比如Input组件,需要做到既支持受控模式,又支持非受控模式。
非受控组件
对于下面的例子,这个Input组件中只有内部有一个状态,没有任何属性接受外部的状态,它是一个非受控的组件。
import React, { useState } from "react";
export const Input: React.FC = () => {
const [value, setValue] = useState("");
return (
<>
<input
value={value}
onChange={(e) => {
const val = e.target.value;
setValue(val);
}}
></input>
当前值为: {value}
</>
);
};
export default function App() {
return (
<div className="App">
<Input></Input>
</div>
);
}
支持受控模式和非受控模式
简单实现
接着,给组件添加一个value属性和onChange属性,让组件同时支持受控模式和非受控模式。
设计思路:
在Input组件中始终维护一个状态,无论是否处于受控模式,它都直接使用自己的状态。当它处于受控模式时,让内部的状态手动和外部状态保持一致。
import React, { useEffect, useState } from "react";
import { isUndefined } from "util";
interface CustomInputComProps {
value?: string;
onChange?: (value: string) => void;
}
export const Input: React.FC<CustomInputComProps> = (props) => {
const [value, setValue] = useState(props.value);
const isControlled = !isUndefined(props.value);
useEffect(() => {
// 如果是受控模式,让内部的状态和外部的状态保持一致
if (isControlled) {
setValue(props.value);
}
}, [props.value]);
return (
<>
<input
value={value}
onChange={(e) => {
const val = e.target.value;
// 如果是非受控模式,自己管理自己的状态
if (!isControlled) setValue(val);
props.onChange?.(val);
}}
></input>
当前值为: {value}
</>
);
};
export default function App() {
const [name, setName] = useState("");
return (
<div className="App">
<Input
value={name}
onChange={(value) => {
setName(value);
}}
></Input>
</div>
);
}
存在的问题:
- 由于通过setState更新子组件的状态,所以子组件状态的更新比父组件晚一个渲染周期。
- 受控模式下,内部状态在useEffect中通过setState来进行状态同步,因此会产生额外的一次渲染,一般来说多一次的渲染带来的性能问题可以忽略不计,但对于复杂的组件,还是可以继续优化。
改进1
解决方式:
- 对于子组件状态的更新比父组件晚一个渲染周期的问题:子组价和父组件的状态同步实际无需时刻一致,只需要判断,在组件处于受控模式下,直接使用来自外部的状态即可。
import React, { useEffect, useState } from "react";
import { isUndefined } from "util";
interface CustomInputComProps {
value?: string;
onChange?: (value: string) => void;
}
export const Input: React.FC<CustomInputComProps> = (props) => {
const [value, setValue] = useState(props.value);
const isControlled = !isUndefined(props.value);
useEffect(() => {
// 如果是受控模式,让内部的状态和外部的状态保持一致
if (isControlled) {
setValue(props.value); //
}
}, [props.value]);
const finalValue = isControlled ? props.value : value;
return (
<>
<input
value={finalValue}
onChange={(e) => {
const val = e.target.value;
// 如果是非受控模式,自己管理自己的状态
if (!isControlled) setValue(val);
props.onChange?.(val);
}}
></input>
当前值为: {value}
</>
);
};
export default function App() {
const [name, setName] = useState("");
return (
<div className="App">
<Input
value={name}
onChange={(value) => {
setName(value);
}}
></Input>
</div>
);
}
改进1的抽象
import React, { useEffect, useState } from "react";
import { isUndefined } from "util";
import { useUpdateEffect } from "ahooks";
type Options<T> = {
value?: T;
defaultValue?: T;
};
export function usePropsValue<T>(
options: Options<T>
): [T, React.Dispatch<React.SetStateAction<T>>] {
const isControlled = !isUndefined(options.value);
const [state, setState] = useState<T>(
isControlled ? options.value : options.defaultValue
);
useUpdateEffect(() => {
// 外部value改变时
// 如果移除了value,则更新内部上
// 否则,不同步到内部状态上,(如果同步到内部状态上,会引起额外的更新)
if (options.value === undefined) {
setState(options.value);
}
}, [options.value]);
const finalValue = isControlled ? options.value : state;
return [finalValue, setState];
}
改进2
对于setState产生额外的更新问题,可以通过ref存数据+手动更新来处理。
import React, { useRef } from "react";
import { isUndefined } from "util";
import { useUpdate } from "ahooks";
type Options<T> = {
value?: T;
defaultValue?: T;
};
export function usePropsValue<T>(options: Options<T>) {
const isControlled = !isUndefined(options.value);
const update = useUpdate();
const stateRef = useRef(isControlled ? options.value : options.defaultValue);
if (isControlled) {
// 受控模式下,同步外部状态到内部状态
stateRef.current = options.value;
}
const setState = (nextValue: T) => {
if (nextValue === stateRef.current) return; // 避免不必要的update
stateRef.current = nextValue;
update(); // 更新组件
};
return [stateRef.current, setState] as const;
}
import React, { useState } from "react";
import { isUndefined } from "util";
import { usePropsValue } from "./usePropsValue";
interface CustomInputComProps {
value?: string;
onChange?: (value: string) => void;
}
export const Input: React.FC<CustomInputComProps> = (props) => {
const isControlled = !isUndefined(props.value);
const [value, setValue] = usePropsValue(props);
return (
<>
<input
value={value}
onChange={(e) => {
const val = e.target.value;
// 如果是非受控模式,自己管理自己的状态
if (!isControlled) {
setValue(val);
}
props.onChange?.(val);
}}
></input>
当前值为: {value}
</>
);
};
export default function App() {
const [name, setName] = useState("");
return (
<div className="App">
<Input
value={name}
onChange={(value) => {
console.log("onChange触发了", value);
setName(value);
}}
></Input>
</div>
);
}
参考
Controlled and uncontrolled form inputs in React don't have to be complicated
React 系列十:受控组件和非受控组件
antd mobile 作者教你写 React 受控组件和非受控组件
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。