几周前我做了一个DevTips with Kent的直播,我向你展示了如何用React钩子将复合组件模式从一个类组件重构为一个函数组件。
如果你不熟悉复合组件,那么你可能还没有看过我在 egghead.io或Frontend 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更具表现力和实用性。祝您好运!