文章是关于React组件之表单单行文本输入框的一些思考。可能大家第一反应都是,不就是一行<input/>嘛,没什么特别的吧?如果说到输入框的值的话,可能圈子里上大多数封装好的ReactUI组件库中使用的方式无非都是在组件中通过 Props传值给Input组件,然后在Input组件的onChange回掉中改变当前组件的State,从而达到改变Input组件Props的目的。
这里引发一个思考,如果父组件相对复杂,当其中一个Input组件的值被改变时,会更新State触发render和其他所有可能与State相关的子组件(这些字组件的Prop都是通过父组件的State来传递的)的更新,简单的想象一下,我们在输入框中每敲一下键盘都会触发一系列的代码逻辑,这可能会是多恐怖的一件事。
通过Props传值的必要性
Input输入框必然是有value这一属性的,如果这个属性是作为State或者Props传递的,要改变这一属性,也是必然通过改变Props或者State来实现,而改变的途径有且只有一个,就是在Input的onChange里面进行监听,正如官方所给的示例代码一样Forms-React:
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}如果我们想要把这个Input输入框封装成为一个单独的组件,按照我们正常的处理逻辑,这个组件的value一定是通过Props传递过来的,当我们把这个Props作为Input的属性值之后,想要改变的话也就只能通过父组件改变传递的Props来实现了,不可避免的产生了一些改变值和传递值的代码逻辑。
一些优秀UI库的常规处理
了解了Input输入框的传值机制之后,我们来看看圈子里一些优秀组件库的处理方式是否和我们预想的一致,是否也有这些不可变的代码逻辑问题。
React-Bootstrap
React-Boostrap算是Bootstrap的React版,其权威性也是可见一斑。官方的示例代码也是完全和我们预期的一致,通过onChang回掉来改变父组件的State,之后再把State作为Props传递给子组件,有兴趣可自行查阅。而我们打开源码来看,关键部分如下:
class FormControl extends React.Component {
render() {
...
const {
componentClass: Component,
type,
id = controlId,
inputRef,
className,
bsSize,
...props
} = this.props;
const [bsProps, elementProps] = splitBsProps(props);
...
return (
<Component
{...elementProps}
type={type}
id={id}
ref={inputRef}
className={classNames(className, classes)}
/>
);
}
}可以解读成就是一个标签的封装,没有自己的State,其值完全依赖于Props的改变,这也和我们预期的结果吻合,而温和的结果就是我们最开始抛出的一些疑问,看来在这里也没得到解决。
ant-design
对比一下国人写的ant-design,直接看官方示例代码,当看到这里的时候是否又些小激动呢,代码如下:
import { Input } from 'antd';
ReactDOM.render(<Input placeholder="Basic usage" />, mountNode);似乎摆脱了在回掉的onChange中改变State来传递Props的方式,代码简洁明了。打开源码来看看,ant-design,截取关键部分代码:
renderInput() {
const { value, className } = this.props;
const otherProps = omit(this.props, [
'prefixCls',
'onPressEnter',
'addonBefore',
'addonAfter',
'prefix',
'suffix',
]);
if ('value' in this.props) {
otherProps.value = fixControlledValue(value);
// Input elements must be either controlled or uncontrolled,
// specify either the value prop, or the defaultValue prop, but not both.
delete otherProps.defaultValue;
}
return this.renderLabeledIcon(
<input
{...otherProps}
className={classNames(this.getInputClassName(), className)}
onKeyDown={this.handleKeyDown}
ref="input"
/>,
);
}可以很明显的看到,整个组件也并没有State,所有的状态也都是通过Props来进行管理的,并没有看出其奇特之处在哪儿。如果我们在官方示例中加入一个属性,value="xxxx",如此一来,想要改变这个值也是和之前的处理如出一辙。
react-ui
也是国人写的一个框架,相对来说名气较小。直接打开其源码来看,react-ui,关键代码如下:
constructor (props) {
super(props)
this.handleChange = this.handleChange.bind(this)
}
handleChange (event) {
const { type } = this.props
let value = event.target.value
if (value && (type === 'integer' || type === 'number')) {
if (!Regs[type].test(value)) return
}
this.props.onChange(value)
}很常规的一种处理,和上面的处理方式唯一不同在于语法上面的一些差异,可能这也并非我们想要看到的。
是否真的有那么一种处理机制和我们预想不一样,并且能够解决我们的疑问呢?
MUI
直接打开源码来看,MUI,关键部分如下:
constructor(props) {
super(props);
let value = props.value;
let innerValue = value || props.defaultValue;
if (innerValue === undefined) innerValue = '';
this.state = {
innerValue: innerValue,
isTouched: false,
isPristine: true
};
...
}
componentWillReceiveProps(nextProps) {
if ('value' in nextProps) this.setState({ innerValue: nextProps.value });
}
onChange(ev) {
this.setState({
innerValue: ev.target.value,
isPristine: false
});
...
}可以看到,这个UI的处理方式和之前所见就都有所不同了,不同之处在于,把传入进来的Props.value又做了额外的处理,塞到State里面去了,这样改变input value的方式就可以只改变当前这个组件的state来进行了,而不需要再次通过改变回掉到父组件改变State,重新传入Props的方式来进行。看到这里是不是有一丝的欣慰呢?但是冷静一想,好像是把问题搞复杂了:
- 为什么需要通过计算传入Props来当作State呢,既然可以通过Props计算得到,拿这个属性应该不是一个State;
- componentWillReceiveProps中又要处理判断一些逻辑,而且这个逻辑很有可能要与自己的业务相关,从而来决定是否需要改变State,这些逻辑并非是我们想要看到的。
metarial-design
所谓扁平化至极的风格,看看源码里面究竟怎么处理这个问题呢,metarial-design:
handleRefInput = node => {
this.input = node;
if (this.props.inputRef) {
this.props.inputRef(node);
}
};
let inputProps = {
ref: this.handleRefInput,
...inputPropsProp,
};
return (
<div onBlur={this.handleBlur} onFocus={this.handleFocus} className={className} {...other}>
<InputComponent
{...inputProps}
/>
</div>
); 其中有一处关键处理和之前看到的都不一样,这个ref属性,直接赋值this.input=input(当然它还有额外的函数处理,这里并不影响),这行代码究竟有什么神奇的效果嘛,应该只是单纯的直接从DOM里面取值或者进行一些直接修改DOM的操作吧?。最后看看官方有没有相关的解释。
React官网
仔细查阅,找到了一篇相关的文章说明Uncontroled Components。这篇文章详细的介绍我们的疑惑(看来这些疑问并非是想当然的,实实在在有这个问题)。
In most cases, we recommend using controlled components to implement forms. In a controlled component, form data is handled by a React component. The alternative is uncontrolled components, where form data is handled by the DOM itself.
看到这里应该就豁然开朗了,所以所谓正确的姿势:
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={(input) => this.input = input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}看了之后,就能理解material-design和ant-design的写法了,如果想要默认值,正如官方所说:
In the React rendering lifecycle, the value attribute on form elements will override the value in the DOM. With an uncontrolled component, you often want React to specify the initial value, but leave subsequent updates uncontrolled. To handle this case, you can specify a defaultValue attribute instead of value.
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input
defaultValue="Bob"
type="text"
ref={(input) => this.input = input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}来回往复,直接一个defaultValue解决了我们的疑惑,原本就在官方网站摆着却看似明白不清楚具体场景,想的太多做的太少。