React Hooks 复合组件

138 阅读2分钟

这个想法是,你有两个或更多的组件,它们一起工作来完成一项任务。通常,一个组件是母体,而另一个是子体。其目的是为了提供一个更具表现力和灵活性的API。

比如像 <select> and <option>:

<select>
  <option value="value1">key1</option>
  <option value="value2">key2</option>
  <option value="value3">key3</option>
</select>

如果你试图使用一个而不使用另一个,那它就无法工作。此外,它实际上是一个非常棒的API。让我们看看,如果我们没有复合组件的API来工作,会是什么样子(记住,这是HTML,不是JSX)。

<select options="key1:value1;key2:value2;key3:value3"></select>

我相信你可以想出其他的方法来表达,但很恶心。那你怎么用这种API来表达disabled 属性呢?这有点疯狂。

因此,复合组件API为你提供了一种表达组件之间关系的好方法。

这方面的另一个重要方面是 "隐性状态 "的概念。<select>元素隐含地存储了关于所选选项的状态,并与它的子元素共享,这样它们就知道如何根据该状态来呈现自己。但这种状态共享是隐性的,因为在我们的HTML代码中没有任何东西可以访问这种状态(反正也不需要)。

好吧,让我们看看一个合法的React组件,它展现了一个复合组件,以进一步理解这些原则。这里有一个Reach UI的<Menu /> 组件的例子,它展现了一个复合组件的API。

function App() {
  return (
    <Menu>
      <MenuButton>
        Actions <span aria-hidden></span>
      </MenuButton>
      <MenuList>
        <MenuItem onSelect={() => alert('Download')}>Download</MenuItem>
        <MenuItem onSelect={() => alert('Copy')}>Create a Copy</MenuItem>
        <MenuItem onSelect={() => alert('Delete')}>Delete</MenuItem>
      </MenuList>
    </Menu>
  )
}

在这个例子中,<Menu>建立了一些共享的隐式状态。<MenuButton><MenuList><MenuItem>组件各自访问或操作该状态,并且都是隐式完成的。这使你可以拥有你所寻找的富有表现力的API。

在讲解一个新的概念时,我喜欢一开始就使用简单的例子。所以我们将使用我最喜欢的<Toggle>组件的例子。

下面是我们的<Toggle>复合组件的使用方法:

function App() {
  return (
    <Toggle onToggle={on => console.log(on)}>
      <ToggleOn>The button is on</ToggleOn>
      <ToggleOff>The button is off</ToggleOff>
      <ToggleButton />
    </Toggle>
  )
}

好了,你们都在等待的时刻,带上下文和钩子的复合组件的全面实现。

import * as React from 'react'
// this switch implements a checkbox input and is not relevant for this example
import {Switch} from '../switch'

const ToggleContext = React.createContext()

function useEffectAfterMount(cb, dependencies) {
  const justMounted = React.useRef(true)
  React.useEffect(() => {
    if (!justMounted.current) {
      return cb()
    }
    justMounted.current = false
  }, dependencies)
}

function Toggle(props) {
  const [on, setOn] = React.useState(false)
  const toggle = React.useCallback(() => setOn(oldOn => !oldOn), [])
  useEffectAfterMount(() => {
    props.onToggle(on)
  }, [on])
  const value = React.useMemo(() => ({on, toggle}), [on])
  return (
    <ToggleContext.Provider value={value}>
      {props.children}
    </ToggleContext.Provider>
  )
}

function useToggleContext() {
  const context = React.useContext(ToggleContext)
  if (!context) {
    throw new Error(
      `Toggle compound components cannot be rendered outside the Toggle component`,
    )
  }
  return context
}

function ToggleOn({children}) {
  const {on} = useToggleContext()
  return on ? children : null
}

function ToggleOff({children}) {
  const {on} = useToggleContext()
  return on ? null : children
}

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

因此,这种工作方式是我们用React创建一个上下文,在那里我们存储状态和一个更新状态的机制。然后<Toggle> 组件负责向 React 树的其他部分提供该上下文值。

阅读原文