通过学习《第一个组件》这一节,相信你已经理解了 props 和 state 的区别。这一节,我们会介绍 “受控组件”和“非受控组件”这两个概念。
非受控组件
我们首先看一个简单的例子,现在有一个输入组件。
const MyInput = ({ onChange }) => (<input onChange={onChange} />);
上面这个组件会显示一个输入框,每次有用户输入,就会调用传入的参数 onChange。
然后,将这个组件放入另一个组件。
class Demo extends React.Component {onTextChange = (event) => {console.log(event.target.value);}render() {return (<MyInput onChange={this.onTextChange} />);}}
上面代码中,我们将 MyInput 组件与一个监听函数 onTextChange 封装在一起。
现在,需要一个重置按钮,点击后可以清空 MyInput 的内容,那么可以像下面这样调整。
<div><MyInput onChange={this.onChange} /><button onClick={this.onTextReset}>Reset</button></div>
onTextReset = () => {// 我该怎么做?// 拿到 MyInput 内部的 input 元素然后设置 value 为 ''?}
看起来,修改 MyInput 中的值不太容易。
对于这种不能直接控制状态的组件,我们称之为“非受控组件”。
受控组件
接着,我们做一些调整。将其改造成受控组件。
const MyInput = ({ value = '', onChange }) => (<input value={value} onChange={onChange} />);
这时, MyInput 的输入完全由 value 属性来决定。
你会发现,新的代码你无法在输入框输入任何东西(因为 value 总是 ‘’)。
我们改造一下 Demo,让它可以重新工作:
class Demo extends React.Component {state = {text: '',}onTextChange = (event) => {this.setState({ text: event.target.value });}render() {return (<MyInput value={this.state.text} onChange={this.onTextChange} />);}}
好了,重置变得轻而易举:
onTextReset = () => {this.setState({ text: '' });}
“受控”与“非受控”两个概念,区别在于这个组件的状态是否可以被外部修改。一个设计得当的组件应该同时支持“受控”与“非受控”两种形式,即当开发者不控制组件属性时,组件自己管理状态,而当开发者控制组件属性时,组件该由属性控制。而开发一个复杂组件更需要注意这点,以避免只有部分属性受控,使其变成一个半受控组件。
tabs 组件
一个典型的组件例子,可以参考 antd 中的 tabs 组件:
<Tabs><TabPane tab="Tab 1" key="1">Content of Tab Pane 1</TabPane><TabPane tab="Tab 2" key="2">Content of Tab Pane 2</TabPane></Tabs>
大部分情况下,开发者都不用考虑如何控制 tabs 停留在哪个标签页,用户在需要时自行点击即可。这种情况下,tabs 会作为“非受控组件”来运行。
而当传递 activeKey 属性时,tabs 组件会转变为“受控组件”。标签切换需要通过代码来进行控制:
<Tabs activeKey={this.state.activeKey} onChange={this.onTabChange}><TabPane tab="Tab 1" key="1">Content of Tab Pane 1</TabPane><TabPane tab="Tab 2" key="2">Content of Tab Pane 2</TabPane></Tabs>
state = {activeKey: '1',}onTabChange = (activeKey) => {this.setState({ activeKey });}
tree 组件
通过控制组件的状态,我们可以实现一些原本组件设计并没有实现的功能。
举个例子,在 tree 组件中。我们通过点击节点左边的小三角进行展开/关闭,点击文字部分是选中该节点:
<Tree><TreeNode title="parent 1" key="0-0"><TreeNode title="leaf" key="0-0-0" /><TreeNode title="leaf" key="0-0-1" /></TreeNode></Tree>
如果我们现在想要改成点击文字部分,同样是展开/关闭节点,应该怎么做呢?
首先,我们查询一下文档,找出与此次需求相关的属性有哪些。
expandedKeys: 设置展开的节点selectedKeys: 设置被选中的节点onExpand: 节点被展开/关闭时触发onSelect: 节点被选中时触发
这很容易就联想到如何进行调整:节点被选中时,将原本修改 selectedKeys 改成更新 expandedKeys。转换成对应的代码:
<TreeexpandedKeys={this.state.expandedKeys}selectedKeys={[]}onExpand={this.onExpand}onSelect={this.onSelect}><TreeNode title="parent 1" key="0-0"><TreeNode title="leaf" key="0-0-0" /><TreeNode title="leaf" key="0-0-1" /></TreeNode></Tree>
state = {expandedKeys: [],}// 接收原本的展开事件,在 state 中记录 expandedKeysonExpand = (expandedKeys) => {this.setState({ expandedKeys });}// 接收选中事件,修改 expandedKeysonSelect = (selectedKeys) => {const { expandedKeys } = this.state;const key = selectedKeys[0];if (expandedKeys.includes(key)) {// 移除 keythis.setState({expandedKeys: expandedKeys.filter(k => k !== key),});} else {// 添加 keythis.setState({ expandedKeys: [...expandedKeys, key] });}}
