什么是 React 复合组件
本文翻译自文章 React Hooks: Compound Components ,略有增减。
如果你还没有接触过复合组件,那么阅读完本文就会对它有了初步的认识。
复合组件是 React 的一个高级模式,通常是由两个或两个以上的组件共同来实现某项功能。其中一个组件作为父组件,其余组件作为它的子组件,利用这种显式父子关系来共享隐式状态。
复合组件支持组件中的状态和逻辑的共享,可以帮助开发人员构建更直观和灵活性的 API,避免了在子组件间用 props 进行通信。
复合组件可以类比 HTML 中的
<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
等其它属性。因此,复合组件为我们提供了一种表达组件间关系的方法。
这其中的另一个重要概念是“隐式状态”。
<select>
元素隐式存储了关于所选
<option>
的状态,并与它的子组件共享该状态,以便它们根据该状态来渲染自己。但这种状态的共享是隐式的,因为在 HTML 代码中没有任何途径可以访问这个状态(而且也不需要去访问)。
好了,让我们来看看一个真实的 React 组件,以进一步了解复合组件的原理。下面是 Reach UI 中的 <Menu /> 组件 的一个示例,它提供复合组件 API:
function App() {
return (
<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 更加优雅。
那么如何实现这样的组件呢?通常有两种方法:第一种是对 children 使用
React.cloneElement
;第二种是使用 React context。在本篇文章中,主要介绍如何使用 React context 创建一组简单的复合组件。
接下来,我们以
<Toggle>
组件为例来介绍实现过程。
<Toggle>
组件包含了
<ToggleOn>
、
<ToggleOff>
和
<ToggleButton />
三个组件,当
<ToggleButton>
被点击时,会根据当前状态来展示
<ToggleOn>
或
<ToggleOff>
中的内容。该组件的使用方式如下:
function App() {
return (
<Toggle onToggle={on => console.log(on)}>
<ToggleOn>The button is on</ToggleOn>
<ToggleOff>The button is off</ToggleOff>
<ToggleButton />
</Toggle>
}
具体效果如下:
接下来,我们来看一下使用 context 和 hooks 来实现
<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