复合组件是什么

编写页面时,经常存在多个子组件的展示,是依赖于同一个数据源的情况。 比如单选框:

<Switcher>
    <Switch on={this.props.selecting == 'React'}>React</Switch>
    <Switch on={this.props.selecting == 'Vue'}>Vue</Switch>
</Switcher>

我们可以看到,所有的 Switch 的数据都需要对 selecting 的值进行判断,并且代码中其实只有 this.props.selecting == 后面的部分不同,如果能改写成这样:

static Switcher.React = ({selecting}) => (
  <Switch on={selecting === 'React'}>React</Switch>
)
static Switcher.Vue = ({selecting}) => (
  <Switch on={selecting === 'Vue'}>Vue</Switch>
)

<Switcher selecting={this.props.selecting}>
    <Switcher.React/>
    <Switcher.Vue/>
</Switcher>

隐式地将父组件的数据传递给子组件,其显示逻辑交由给子组件自行处理,代码的组织结构将会清晰很多。后续即使需求变动,数据的传递改变也并不需要我们操心(不需要一个子组件一个子组件地添加传递),只需要修改 Switcher 子控件内部处理逻辑即可。

那么要怎么实现这个隐式数据传递呢? 可以通过 React.Children.mapReact.cloneElement 这两个 API 来实现。

React.Children.map 与 React.cloneElement

render 中我们可以使用 React.Children.map 来获取到 Switcher 中的子组件,然后通过 React.cloneElement 对组件进行克隆及数据传递:

render() {
    return React.Children.map(this.props.children, child =>
      React.cloneElement(child, {
        on: this.state.on,
        toggle: this.toggle,
      }),
    )
  }

这样,即使我们在使用 Switcher.ReactSwitcher.Vue 时,没有显式地传递参数,子组件也能获取数据。

这里 React.Children.mapthis.props.children.map 并不等价,后者在只有一个子组件的时候,返回的不是数组,而是唯一的那个组件。

React.Children.map 的局限性

上面代码有个问题是,如果出现了更多层级的子组件,那么参数传递只会到第一层。

<Switcher selecting={this.props.selecting}>
    <Switcher.React/>
    <div>
      <Switcher.Vue/>
    </div>
</Switcher>

这样写会提示传递了错误的参数给 div,因为我们 React.Children.map 只能获取到第一层子组件([Switcher.React, div])。

那怎么办,难道要用递归?React 16.x 提供了新的 Context 可以很好地解决这个问题。

Context 的使用方法很简单,首先创建一个 Context:

const selecting = ""
const SwitcherContext = React.createContext("")

接着是 render,既然我们不确定会有多少层的子组件,那么就直接将 this.props.children 包裹在 Context.Provider 中:

<SwitcherContext.Provider value={this.props.selecting}>
  {this.props.children}
</SwitcherContext.Provider>

然后改写我们的子组件数据获取方式,之前是通过 React.cloneElement 来将数据通过 props 传递到组件中,现在可以直接从 Context.Consumer 中获取:

static Switcher.React =() => (
  <SwitcherContext.Consumer>
    { selecting => (
      <Switch on={selecting === 'React'}>React</Switch>) 
    }
  </SwitcherContext.Consumer>
)

如此一来就完成了我们的改造。外部使用到 Switcher 的地方没有任何变动,依然是:

<Switcher selecting={this.props.selecting}>
    <Switcher.React/>
    <div>
      <Switcher.Vue/>
    </div>
</Switcher>

相关资料