React Hooks复合组件的简介(附实例)

82 阅读4分钟

几周前我做了一个DevTips with Kent的直播,我向你展示了如何用React钩子将复合组件模式从一个类组件重构为一个函数组件。

如果你不熟悉复合组件,那么你可能还没有看过我在 egghead.ioFrontend Masters上的高级 React 组件模式课程。

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

可以把它看作是<select><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。

那么这是如何做到的呢?好吧,如果你看了我的课程,我向你展示了两种方法。一种是用React.cloneElement ,另一种是用React context。(我的课程需要稍作更新,以展示如何用钩子来做这件事)。在这篇博文中,我将向你展示如何使用上下文创建一个简单的复合组件集。

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

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

function App() {
  return (
    <Toggle onToggle={on => console.log(on)}>
      <Toggle.On>The button is on</Toggle.On>
      <Toggle.Off>The button is off</Toggle.Off>
      <Toggle.Button />
    </Toggle>
  )
}

你会注意到,我们的组件名称中使用了. 。这是因为这些组件是作为静态属性添加到<Toggle> 组件中的。请注意,这根本不是复合组件的要求(上面的<Menu>组件就不这样做)。我只是喜欢这样做,作为一种明确沟通关系的方式。

好了,你们都在等待的时刻,带上下文和钩子的复合组件的实际完整实现:

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 On({children}) {
  const {on} = useToggleContext()
  return on ? children : null
}

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

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

// for convenience, but totally not required...
Toggle.On = On
Toggle.Off = Off
Toggle.Button = Button

下面是这个组件的操作。

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

我将在我的高级React组件模式课程的未来更新中演练这个实现并解释具体细节。所以,请留意这篇文章吧

我希望这能帮助你获得一些想法,使你的组件API更具表现力和实用性。祝您好运!