通过学习《第一个组件》这一节,相信你已经理解了 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
。转换成对应的代码:
<Tree
expandedKeys={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 中记录 expandedKeys
onExpand = (expandedKeys) => {
this.setState({ expandedKeys });
}
// 接收选中事件,修改 expandedKeys
onSelect = (selectedKeys) => {
const { expandedKeys } = this.state;
const key = selectedKeys[0];
if (expandedKeys.includes(key)) {
// 移除 key
this.setState({
expandedKeys: expandedKeys.filter(k => k !== key),
});
} else {
// 添加 key
this.setState({ expandedKeys: [...expandedKeys, key] });
}
}