受控组件与非受控组件

通过学习《第一个组件》这一节,相信你已经理解了 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] });
  }
}