© Bohua Xu

Compound Components

Jul 26, 2022 · 6min

复合组件模式使您能够为可重用组件的简单而强大的声明性 API 提供一组隐式共享状态的组件。

// Compound Components
import * as React from 'react'
import { Switch } from './switch'

function Toggle({children}) {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)
  return React.Children.map(children, child => {
    return typeof child.type === 'string'            // 💯 支持 DOM 子组件
      ? child
      : React.cloneElement(child, {on, toggle})
  })
}

const ToggleOn = ({on,children}) => on ? children : null

const ToggleOff = ({on,children}) => on ?  null  : children

const ToggleButton = ({on,toggle}) => {
return <Switch on={on} onClick={toggle} />
}

function App() {
  return (
    <div>
      <Toggle>
        <ToggleOn>The button is on</ToggleOn>
        <ToggleOff>The button is off</ToggleOff>
        {/* <span>5555</span> */}
        <ToggleButton />
      </Toggle>
    </div>
  )
}

export default App;
// Compound Components
import * as React from 'react'
import { Switch } from './switch'

function Toggle({children}) {
  const [on, setOn] = React.useState(false)
  const toggle = () => setOn(!on)
  return React.Children.map(children, child => {
    return typeof child.type === 'string'            // 💯 支持 DOM 子组件
      ? child
      : React.cloneElement(child, {on, toggle})
  })
}

const ToggleOn = ({on,children}) => on ? children : null

const ToggleOff = ({on,children}) => on ?  null  : children

const ToggleButton = ({on,toggle}) => {
return <Switch on={on} onClick={toggle} />
}

function App() {
  return (
    <div>
      <Toggle>
        <ToggleOn>The button is on</ToggleOn>
        <ToggleOff>The button is off</ToggleOff>
        {/* <span>5555</span> */}
        <ToggleButton />
      </Toggle>
    </div>
  )
}

export default App;

📝

**One liner:**复合组件模式使您能够为可重用组件的简单而强大的声明性 API 提供一组隐式共享状态的组件。

复合组件是协同工作以形成完整 UI 的组件。<select>典型的例子是<option>HTML:

<select>
  <option value="1">Option 1</option>
  <option value="2">Option 2</option>
</select>
<select>
  <option value="1">Option 1</option>
  <option value="2">Option 2</option>
</select>

<select>是负责管理 UI 状态的元素,这些元素<option>本质上更多的是配置选择应该如何操作(具体来说,哪些选项可用及其值)。

假设我们要手动实现这个原生控件。一个简单的实现看起来像这样:

<CustomSelect
  options={[
    {value: '1', display: 'Option 1'},
    {value: '2', display: 'Option 2'},
  ]}
/>
<CustomSelect
  options={[
    {value: '1', display: 'Option 1'},
    {value: '2', display: 'Option 2'},
  ]}
/>

这很好用,但它的可扩展性/灵活性不如复合组件 API。例如。如果我想在 <option>渲染的对象上提供额外的属性,或者我想display根据它是否被选中而改变呢?我们可以轻松地添加 API 表面积来支持这些用例,但这只是我们编写代码和用户学习的更多内容。这就是复合组件真正派上用场的地方!

使用此模式的真实项目:

每个可重用组件都是从针对特定用例的简单实现开始的。建议不要使组件过于复杂,并尝试解决还没有(并且可能永远不会有)的所有可能的问题。但是随着变化的到来(而且几乎总是如此)

这就是为什么我们从一个超级简单的<Toggle />组件开始。

在本练习中,我们将创建<Toggle />一些复合组件的父级:

  • <ToggleOn />on状态为true
  • <ToggleOff />on状态为false
  • <ToggleButton />将prop<Switch />设置onon state 并将onClickprop 设置为toggle

我们有一个 Toggle 组件来管理状态,并且我们想要渲染 UI 的不同部分。我们想要控制 UI 的呈现。

🦉 使用这样的 API 面临的基本挑战是组件之间共享的状态是隐式的,这意味着使用组件的开发人员实际上无法看到或与状态 ( on) 或用于更新该状态 ( toggle)的机制进行交互。组件之间共享。

我们将通过为复合组件提供他们需要隐式使用的道具来解决这个问题React.cloneElement

React.Children.map这是一个使用and的简单示例React.cloneElement

function Foo({children}) {
  return React.Children.map(children, (child, index) => {
    return React.cloneElement(child, {
      id: `i-am-child-${index}`,
    })
  })
}

function Bar() {
  return (
    <Foo>
      <div>I will have id "i-am-child-0"</div>
      <div>I will have id "i-am-child-1"</div>
      <div>I will have id "i-am-child-2"</div>
    </Foo>
  )
}
function Foo({children}) {
  return React.Children.map(children, (child, index) => {
    return React.cloneElement(child, {
      id: `i-am-child-${index}`,
    })
  })
}

function Bar() {
  return (
    <Foo>
      <div>I will have id "i-am-child-0"</div>
      <div>I will have id "i-am-child-1"</div>
      <div>I will have id "i-am-child-2"</div>
    </Foo>
  )
}

🦉

CC BY-NC-SA 4.0 2021-PRESENT © Bohua Xu