通过学习《第一个组件》这一节,相信你已经理解了 props 和 state 的区别。这一节,我们会介绍 “受控组件”和“非受控组件”这两个概念。

非受控组件

我们首先看一个简单的例子,现在有一个输入组件。

  1. const MyInput = ({ onChange }) => (
  2. <input onChange={onChange} />
  3. );

上面这个组件会显示一个输入框,每次有用户输入,就会调用传入的参数 onChange

然后,将这个组件放入另一个组件。

  1. class Demo extends React.Component {
  2. onTextChange = (event) => {
  3. console.log(event.target.value);
  4. }
  5. render() {
  6. return (
  7. <MyInput onChange={this.onTextChange} />
  8. );
  9. }
  10. }

上面代码中,我们将 MyInput 组件与一个监听函数 onTextChange 封装在一起。

现在,需要一个重置按钮,点击后可以清空 MyInput 的内容,那么可以像下面这样调整。

  1. <div>
  2. <MyInput onChange={this.onChange} />
  3. <button onClick={this.onTextReset}>Reset</button>
  4. </div>
  1. onTextReset = () => {
  2. // 我该怎么做?
  3. // 拿到 MyInput 内部的 input 元素然后设置 value 为 ''?
  4. }

看起来,修改 MyInput 中的值不太容易。

对于这种不能直接控制状态的组件,我们称之为“非受控组件”。

受控组件

接着,我们做一些调整。将其改造成受控组件。

  1. const MyInput = ({ value = '', onChange }) => (
  2. <input value={value} onChange={onChange} />
  3. );

这时, MyInput 的输入完全由 value 属性来决定。

你会发现,新的代码你无法在输入框输入任何东西(因为 value 总是 ‘’)。

我们改造一下 Demo,让它可以重新工作:

  1. class Demo extends React.Component {
  2. state = {
  3. text: '',
  4. }
  5. onTextChange = (event) => {
  6. this.setState({ text: event.target.value });
  7. }
  8. render() {
  9. return (
  10. <MyInput value={this.state.text} onChange={this.onTextChange} />
  11. );
  12. }
  13. }

好了,重置变得轻而易举:

  1. onTextReset = () => {
  2. this.setState({ text: '' });
  3. }

“受控”与“非受控”两个概念,区别在于这个组件的状态是否可以被外部修改。一个设计得当的组件应该同时支持“受控”与“非受控”两种形式,即当开发者不控制组件属性时,组件自己管理状态,而当开发者控制组件属性时,组件该由属性控制。而开发一个复杂组件更需要注意这点,以避免只有部分属性受控,使其变成一个半受控组件。

tabs 组件

一个典型的组件例子,可以参考 antd 中的 tabs 组件

  1. <Tabs>
  2. <TabPane tab="Tab 1" key="1">Content of Tab Pane 1</TabPane>
  3. <TabPane tab="Tab 2" key="2">Content of Tab Pane 2</TabPane>
  4. </Tabs>

大部分情况下,开发者都不用考虑如何控制 tabs 停留在哪个标签页,用户在需要时自行点击即可。这种情况下,tabs 会作为“非受控组件”来运行。

而当传递 activeKey 属性时,tabs 组件会转变为“受控组件”。标签切换需要通过代码来进行控制:

  1. <Tabs activeKey={this.state.activeKey} onChange={this.onTabChange}>
  2. <TabPane tab="Tab 1" key="1">Content of Tab Pane 1</TabPane>
  3. <TabPane tab="Tab 2" key="2">Content of Tab Pane 2</TabPane>
  4. </Tabs>
  1. state = {
  2. activeKey: '1',
  3. }
  4. onTabChange = (activeKey) => {
  5. this.setState({ activeKey });
  6. }

tree 组件

通过控制组件的状态,我们可以实现一些原本组件设计并没有实现的功能。

举个例子,在 tree 组件中。我们通过点击节点左边的小三角进行展开/关闭,点击文字部分是选中该节点:

  1. <Tree>
  2. <TreeNode title="parent 1" key="0-0">
  3. <TreeNode title="leaf" key="0-0-0" />
  4. <TreeNode title="leaf" key="0-0-1" />
  5. </TreeNode>
  6. </Tree>

如果我们现在想要改成点击文字部分,同样是展开/关闭节点,应该怎么做呢?

首先,我们查询一下文档,找出与此次需求相关的属性有哪些。

  • expandedKeys: 设置展开的节点
  • selectedKeys: 设置被选中的节点
  • onExpand: 节点被展开/关闭时触发
  • onSelect: 节点被选中时触发

这很容易就联想到如何进行调整:节点被选中时,将原本修改 selectedKeys 改成更新 expandedKeys。转换成对应的代码:

  1. <Tree
  2. expandedKeys={this.state.expandedKeys}
  3. selectedKeys={[]}
  4. onExpand={this.onExpand}
  5. onSelect={this.onSelect}
  6. >
  7. <TreeNode title="parent 1" key="0-0">
  8. <TreeNode title="leaf" key="0-0-0" />
  9. <TreeNode title="leaf" key="0-0-1" />
  10. </TreeNode>
  11. </Tree>
  1. state = {
  2. expandedKeys: [],
  3. }
  4. // 接收原本的展开事件,在 state 中记录 expandedKeys
  5. onExpand = (expandedKeys) => {
  6. this.setState({ expandedKeys });
  7. }
  8. // 接收选中事件,修改 expandedKeys
  9. onSelect = (selectedKeys) => {
  10. const { expandedKeys } = this.state;
  11. const key = selectedKeys[0];
  12. if (expandedKeys.includes(key)) {
  13. // 移除 key
  14. this.setState({
  15. expandedKeys: expandedKeys.filter(k => k !== key),
  16. });
  17. } else {
  18. // 添加 key
  19. this.setState({ expandedKeys: [...expandedKeys, key] });
  20. }
  21. }