这个想法是,你有两个或更多的组件,它们一起工作来完成一项任务。通常,一个组件是母体,而另一个是子体。其目的是为了提供一个更具表现力和灵活性的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 树的其他部分提供该上下文值。